From 0e451d3452bdaf013304190eb6c42c21235b6713 Mon Sep 17 00:00:00 2001
From: Shai Almog <67850168+shai-almog@users.noreply.github.com>
Date: Sat, 11 Jul 2026 16:50:24 +0300
Subject: [PATCH 01/15] Add portable accessibility semantics
---
.../impl/CodenameOneImplementation.java | 23 +
CodenameOne/src/com/codename1/ui/Button.java | 5 +
.../src/com/codename1/ui/CheckBox.java | 1 +
.../src/com/codename1/ui/Component.java | 82 ++-
CodenameOne/src/com/codename1/ui/Display.java | 19 +
CodenameOne/src/com/codename1/ui/Label.java | 4 +
CodenameOne/src/com/codename1/ui/List.java | 36 +
.../src/com/codename1/ui/RadioButton.java | 1 +
CodenameOne/src/com/codename1/ui/Slider.java | 4 +
CodenameOne/src/com/codename1/ui/Tabs.java | 2 +
.../src/com/codename1/ui/TextArea.java | 2 +
.../ui/accessibility/AccessibilityAction.java | 63 ++
.../AccessibilityAssertions.java | 150 ++++
.../AccessibilityCheckedState.java | 12 +
.../AccessibilityChildProvider.java | 13 +
.../AccessibilityCollectionInfo.java | 43 ++
.../AccessibilityCollectionItemInfo.java | 41 ++
.../accessibility/AccessibilityGrouping.java | 20 +
.../accessibility/AccessibilityInspector.java | 20 +
.../ui/accessibility/AccessibilityIssue.java | 31 +
.../AccessibilityLiveRegion.java | 11 +
.../accessibility/AccessibilityManager.java | 669 ++++++++++++++++++
.../ui/accessibility/AccessibilityNode.java | 197 ++++++
.../AccessibilityNodeSnapshot.java | 176 +++++
.../ui/accessibility/AccessibilityRange.java | 55 ++
.../ui/accessibility/AccessibilityRole.java | 48 ++
.../AccessibilityTreeSnapshot.java | 166 +++++
.../android/AndroidAccessibilityProvider.java | 361 ++++++++++
.../impl/android/AndroidImplementation.java | 36 +-
.../impl/javase/ComponentTreeInspector.java | 54 ++
.../impl/javase/JavaSEAccessibility.java | 290 ++++++++
.../com/codename1/impl/javase/JavaSEPort.java | 27 +
.../impl/html5/HTML5Implementation.java | 201 ++++++
Ports/LinuxPort/nativeSources/cn1_linux.h | 3 +-
.../nativeSources/cn1_linux_window.c | 222 ++++++
.../impl/linux/LinuxImplementation.java | 51 ++
.../com/codename1/impl/linux/LinuxNative.java | 7 +
Ports/WindowsPort/nativeSources/cn1_windows.h | 4 +-
.../cn1_windows_accessibility.cpp | 266 +++++++
.../nativeSources/cn1_windows_window.cpp | 2 +
.../impl/windows/WindowsImplementation.java | 51 ++
.../codename1/impl/windows/WindowsNative.java | 7 +
Ports/iOSPort/nativeSources/IOSNative.m | 211 ++++++
.../codename1/impl/ios/IOSImplementation.java | 16 +
.../src/com/codename1/impl/ios/IOSNative.java | 2 +
.../Accessibility-Semantics.asciidoc | 218 ++++++
docs/developer-guide/developer-guide.asciidoc | 2 +
.../AccessibilitySemanticsTest.java | 182 +++++
.../accessibility/AccessibilityTest.java | 235 +++++-
49 files changed, 4330 insertions(+), 12 deletions(-)
create mode 100644 CodenameOne/src/com/codename1/ui/accessibility/AccessibilityAction.java
create mode 100644 CodenameOne/src/com/codename1/ui/accessibility/AccessibilityAssertions.java
create mode 100644 CodenameOne/src/com/codename1/ui/accessibility/AccessibilityCheckedState.java
create mode 100644 CodenameOne/src/com/codename1/ui/accessibility/AccessibilityChildProvider.java
create mode 100644 CodenameOne/src/com/codename1/ui/accessibility/AccessibilityCollectionInfo.java
create mode 100644 CodenameOne/src/com/codename1/ui/accessibility/AccessibilityCollectionItemInfo.java
create mode 100644 CodenameOne/src/com/codename1/ui/accessibility/AccessibilityGrouping.java
create mode 100644 CodenameOne/src/com/codename1/ui/accessibility/AccessibilityInspector.java
create mode 100644 CodenameOne/src/com/codename1/ui/accessibility/AccessibilityIssue.java
create mode 100644 CodenameOne/src/com/codename1/ui/accessibility/AccessibilityLiveRegion.java
create mode 100644 CodenameOne/src/com/codename1/ui/accessibility/AccessibilityManager.java
create mode 100644 CodenameOne/src/com/codename1/ui/accessibility/AccessibilityNode.java
create mode 100644 CodenameOne/src/com/codename1/ui/accessibility/AccessibilityNodeSnapshot.java
create mode 100644 CodenameOne/src/com/codename1/ui/accessibility/AccessibilityRange.java
create mode 100644 CodenameOne/src/com/codename1/ui/accessibility/AccessibilityRole.java
create mode 100644 CodenameOne/src/com/codename1/ui/accessibility/AccessibilityTreeSnapshot.java
create mode 100644 Ports/Android/src/com/codename1/impl/android/AndroidAccessibilityProvider.java
create mode 100644 Ports/JavaSE/src/com/codename1/impl/javase/JavaSEAccessibility.java
create mode 100644 Ports/WindowsPort/nativeSources/cn1_windows_accessibility.cpp
create mode 100644 docs/developer-guide/Accessibility-Semantics.asciidoc
create mode 100644 maven/core-unittests/src/test/java/com/codename1/ui/accessibility/AccessibilitySemanticsTest.java
diff --git a/CodenameOne/src/com/codename1/impl/CodenameOneImplementation.java b/CodenameOne/src/com/codename1/impl/CodenameOneImplementation.java
index e2be5e7049e..70b3886b129 100644
--- a/CodenameOne/src/com/codename1/impl/CodenameOneImplementation.java
+++ b/CodenameOne/src/com/codename1/impl/CodenameOneImplementation.java
@@ -91,6 +91,8 @@
import com.codename1.ui.TextArea;
import com.codename1.ui.TextSelection;
import com.codename1.ui.Transform;
+import com.codename1.ui.accessibility.AccessibilityManager;
+import com.codename1.ui.accessibility.AccessibilityTreeSnapshot;
import com.codename1.ui.animations.Animation;
import com.codename1.ui.animations.Transition;
import com.codename1.ui.events.ActionEvent;
@@ -11132,6 +11134,27 @@ public void announceForAccessibility(Component cmp, String text) {
// should override this method.
}
+ /// Called after the portable semantic tree changes. Platform ports should
+ /// invalidate their native virtual accessibility roots and request the latest
+ /// immutable snapshot with {@link #getAccessibilityTreeSnapshot()}.
+ public void accessibilityTreeChanged(int changeType) {
+ }
+
+ /// Returns the latest immutable semantic tree for the current form.
+ public AccessibilityTreeSnapshot getAccessibilityTreeSnapshot() {
+ return AccessibilityManager.getInstance().getCurrentSnapshot();
+ }
+
+ /// Dispatches an action from a native virtual accessibility node onto the EDT.
+ public boolean performAccessibilityAction(long nodeId, String actionId, Object argument) {
+ return AccessibilityManager.getInstance().performAction(nodeId, actionId, argument);
+ }
+
+ /// Returns true when this port exposes the portable virtual semantic tree.
+ public boolean isAccessibilityTreeSupported() {
+ return false;
+ }
+
/// Returns true if the user has selected larger type fonts in the system settings.
/// Default implementation returns false.
///
diff --git a/CodenameOne/src/com/codename1/ui/Button.java b/CodenameOne/src/com/codename1/ui/Button.java
index 3d1549f9a3f..33b0f255c62 100644
--- a/CodenameOne/src/com/codename1/ui/Button.java
+++ b/CodenameOne/src/com/codename1/ui/Button.java
@@ -992,7 +992,12 @@ public boolean isToggle() {
///
/// - `toggle`: the toggle to set
public void setToggle(boolean toggle) {
+ if (this.toggle == toggle) {
+ return;
+ }
this.toggle = toggle;
+ accessibilityChanged(com.codename1.ui.accessibility.AccessibilityManager.CHANGE_STRUCTURE
+ | com.codename1.ui.accessibility.AccessibilityManager.CHANGE_STATE);
if (toggle && "CheckBox".equals(getUIID()) || "RadioButton".equals(getUIID())) {
setUIID("ToggleButton");
}
diff --git a/CodenameOne/src/com/codename1/ui/CheckBox.java b/CodenameOne/src/com/codename1/ui/CheckBox.java
index 5144a89f09f..90c2cd94e46 100644
--- a/CodenameOne/src/com/codename1/ui/CheckBox.java
+++ b/CodenameOne/src/com/codename1/ui/CheckBox.java
@@ -173,6 +173,7 @@ public void setSelected(boolean selected) {
this.selected = selected;
if (changed) {
fireChangeEvent();
+ accessibilityChanged(com.codename1.ui.accessibility.AccessibilityManager.CHANGE_STATE);
}
repaint();
}
diff --git a/CodenameOne/src/com/codename1/ui/Component.java b/CodenameOne/src/com/codename1/ui/Component.java
index 91e8f48fa9f..2b0aa338db5 100644
--- a/CodenameOne/src/com/codename1/ui/Component.java
+++ b/CodenameOne/src/com/codename1/ui/Component.java
@@ -40,6 +40,8 @@
import com.codename1.ui.events.StyleListener;
import com.codename1.ui.geom.Dimension;
import com.codename1.ui.geom.Rectangle;
+import com.codename1.ui.accessibility.AccessibilityManager;
+import com.codename1.ui.accessibility.AccessibilityNode;
import com.codename1.ui.layouts.FlowLayout;
import com.codename1.ui.plaf.Border;
import com.codename1.ui.plaf.LookAndFeel;
@@ -424,6 +426,7 @@ static int rubberBandDecompress(int compressed, int dim) {
private EventDispatcher stateChangeListeners;
private String tooltip;
private String accessibilityText;
+ private AccessibilityNode semantics;
/// The native overlay object. Used in Javascript port for some components so that there is
/// an inivisible "native" peer overlaid on the component itself to catch events. E.g.
/// TextFields on iOS can't be programmatically focused except through a user-initiated event -
@@ -1089,7 +1092,11 @@ public int getX() {
///
/// - `x`: the current x coordinate of the components origin
public void setX(int x) {
+ if (bounds.getX() == x) {
+ return;
+ }
bounds.setX(x);
+ accessibilityChanged(AccessibilityManager.CHANGE_BOUNDS);
if (Form.activePeerCount > 0) {
onParentPositionChange();
}
@@ -1132,7 +1139,11 @@ public int getY() {
///
/// - `y`: the current y coordinate of the components origin
public void setY(int y) {
+ if (bounds.getY() == y) {
+ return;
+ }
bounds.setY(y);
+ accessibilityChanged(AccessibilityManager.CHANGE_BOUNDS);
if (Form.activePeerCount > 0) {
onParentPositionChange();
}
@@ -1173,7 +1184,11 @@ public boolean isVisible() {
///
/// - `visible`: true if component is visible; otherwise false
public void setVisible(boolean visible) {
+ if (this.visible == visible) {
+ return;
+ }
this.visible = visible;
+ accessibilityChanged(AccessibilityManager.CHANGE_STRUCTURE);
}
void getVisibleRect(Rectangle r, boolean init) {
@@ -1363,7 +1378,11 @@ public int getWidth() {
///
/// - #setPreferredSize
public void setWidth(int width) {
+ if (bounds.getSize().getWidth() == width) {
+ return;
+ }
bounds.getSize().setWidth(width);
+ accessibilityChanged(AccessibilityManager.CHANGE_BOUNDS);
}
/// Gets the outer width of this component. This is the width of the component including horizontal margins.
@@ -1407,7 +1426,11 @@ public int getHeight() {
///
/// - #setPreferredSize
public void setHeight(int height) {
+ if (bounds.getSize().getHeight() == height) {
+ return;
+ }
bounds.getSize().setHeight(height);
+ accessibilityChanged(AccessibilityManager.CHANGE_BOUNDS);
}
/// Gets the outer height of this component. This is the height of the component including vertical margins.
@@ -2038,6 +2061,7 @@ void setParent(Container parent) {
throw new IllegalArgumentException("Attempt to add self as parent");
}
this.parent = parent;
+ accessibilityChanged(AccessibilityManager.CHANGE_STRUCTURE);
}
/// Gets the "owner" of this component as set by `#setOwner(com.codename1.ui.Component)`.
@@ -2329,7 +2353,8 @@ public void setLabelForComponent(Label componentLabel) {
void focusGainedInternal() {
startComponentLableTicker();
String text = getAccessibilityText();
- if (text != null && text.length() > 0) {
+ if (text != null && text.length() > 0
+ && !Display.getInstance().isAccessibilityTreeSupported()) {
announceForAccessibility(text);
}
}
@@ -3556,6 +3581,9 @@ protected void setScrollX(int scrollX) {
// component back into view can't recurse infinitely (issue #5305).
int oldScrollX = this.scrollX;
this.scrollX = scrollXtmp;
+ if (oldScrollX != scrollXtmp) {
+ accessibilityChanged(AccessibilityManager.CHANGE_BOUNDS);
+ }
if (scrollListeners != null && oldScrollX != scrollXtmp) {
scrollListeners.fireScrollEvent(scrollXtmp, this.scrollY, oldScrollX, this.scrollY);
}
@@ -3581,6 +3609,7 @@ public int getScrollY() {
///
/// - `scrollY`: the Y position of the scrolling
protected void setScrollY(int scrollY) {
+ int oldAccessibilityScrollY = this.scrollY;
if (this.scrollY != scrollY) {
CodenameOneImplementation ci = Display.impl;
@@ -3599,6 +3628,9 @@ protected void setScrollY(int scrollY) {
scrollYtmp = Math.min(scrollYtmp, h);
scrollYtmp = Math.max(scrollYtmp, 0);
}
+ if (oldAccessibilityScrollY != scrollYtmp) {
+ accessibilityChanged(AccessibilityManager.CHANGE_BOUNDS);
+ }
if (isScrollableY()) {
if (Form.activePeerCount > 0) {
onParentPositionChange();
@@ -4188,7 +4220,11 @@ public boolean hasFocus() {
///
/// - #requestFocus
public void setFocus(boolean focused) {
+ if (this.focused == focused) {
+ return;
+ }
this.focused = focused;
+ accessibilityChanged(AccessibilityManager.CHANGE_FOCUS);
}
/// Returns the Component Form or null if this Component
@@ -8125,6 +8161,7 @@ public void setEnabled(boolean enabled) {
return;
}
this.enabled = enabled;
+ accessibilityChanged(AccessibilityManager.CHANGE_STATE);
repaint();
}
@@ -8964,6 +9001,9 @@ public void announceForAccessibility(String text) {
///
/// accessibility description or `null` if none
public String getAccessibilityText() {
+ if (semantics != null && semantics.getLabel() != null) {
+ return semantics.getLabel();
+ }
if (accessibilityText != null) {
return accessibilityText;
}
@@ -8991,6 +9031,46 @@ public String getAccessibilityText() {
/// - `text`: accessibility description
public void setAccessibilityText(String text) {
this.accessibilityText = text;
+ if (semantics != null) {
+ semantics.setLabel(text);
+ } else {
+ accessibilityChanged(AccessibilityManager.CHANGE_CONTENT);
+ }
+ }
+
+ /// Returns the portable semantic configuration for this component. The same
+ /// instance is retained for the lifetime of the component. Mutating it updates
+ /// VoiceOver, TalkBack, UI Automation, AT-SPI, Java accessibility, and web ARIA
+ /// through the active platform adapter.
+ ///
+ /// #### Returns
+ ///
+ /// this component's semantic node
+ public AccessibilityNode getSemantics() {
+ if (semantics == null) {
+ semantics = new AccessibilityNode(this);
+ if (accessibilityText != null) {
+ semantics.setLabel(accessibilityText);
+ }
+ }
+ return semantics;
+ }
+
+ /// Alias for {@link #getSemantics()} for APIs that use accessibility-node terminology.
+ public AccessibilityNode getAccessibilityNode() {
+ return getSemantics();
+ }
+
+ /// Marks this component's semantic representation dirty. Custom components
+ /// whose accessible value is computed dynamically can call this after state
+ /// changes that do not pass through a semantic setter.
+ public void accessibilityChanged() {
+ accessibilityChanged(AccessibilityManager.CHANGE_ALL);
+ }
+
+ /// Marks a specific portion of this component's semantic representation dirty.
+ public void accessibilityChanged(int changeType) {
+ AccessibilityManager.getInstance().invalidate(this, changeType);
}
/// #### Returns
diff --git a/CodenameOne/src/com/codename1/ui/Display.java b/CodenameOne/src/com/codename1/ui/Display.java
index 2960da32e62..8cdce2cf3ec 100644
--- a/CodenameOne/src/com/codename1/ui/Display.java
+++ b/CodenameOne/src/com/codename1/ui/Display.java
@@ -633,6 +633,22 @@ public void announceForAccessibility(String text) {
announceForAccessibility(null, text);
}
+ /// Notifies the native port that the portable semantic tree changed. This is
+ /// primarily an internal bridge used by the accessibility subsystem.
+ ///
+ /// #### Parameters
+ ///
+ /// - `changeType`: bit mask of `AccessibilityManager.CHANGE_*` constants
+ public void accessibilityTreeChanged(int changeType) {
+ impl.accessibilityTreeChanged(changeType);
+ }
+
+ /// Returns true when the active port exposes lightweight components through
+ /// a native virtual accessibility tree.
+ public boolean isAccessibilityTreeSupported() {
+ return impl.isAccessibilityTreeSupported();
+ }
+
/// Returns the status of the show during edit flag
///
/// #### Returns
@@ -1730,6 +1746,9 @@ void setCurrentForm(Form newForm) {
lastKeyPressed = 0;
previousKeyPressed = 0;
newForm.onShowCompletedImpl();
+ com.codename1.ui.accessibility.AccessibilityManager.getInstance().invalidate(
+ newForm, com.codename1.ui.accessibility.AccessibilityManager.CHANGE_STRUCTURE
+ | com.codename1.ui.accessibility.AccessibilityManager.CHANGE_PANE);
}
private boolean applyInitialWindowSize(Form form) {
diff --git a/CodenameOne/src/com/codename1/ui/Label.java b/CodenameOne/src/com/codename1/ui/Label.java
index 4ba6fd7b390..72e6e84cfe2 100644
--- a/CodenameOne/src/com/codename1/ui/Label.java
+++ b/CodenameOne/src/com/codename1/ui/Label.java
@@ -639,11 +639,15 @@ public String getText() {
/// - `text`: the string that the label presents.
@Override
public void setText(String text) {
+ String oldText = this.text;
widthAtLastCheck = -1;
this.text = text;
localize();
stringWidthUnselected = -1;
setShouldCalcPreferredSize(true);
+ if (oldText == null ? this.text != null : !oldText.equals(this.text)) {
+ accessibilityChanged(com.codename1.ui.accessibility.AccessibilityManager.CHANGE_CONTENT);
+ }
repaint();
}
diff --git a/CodenameOne/src/com/codename1/ui/List.java b/CodenameOne/src/com/codename1/ui/List.java
index 0467aa680da..5980b92ca6c 100644
--- a/CodenameOne/src/com/codename1/ui/List.java
+++ b/CodenameOne/src/com/codename1/ui/List.java
@@ -447,6 +447,37 @@ public int size() {
return model.getSize();
}
+ /// Returns the bounds of a renderer-backed row for the portable virtual
+ /// accessibility tree. Bounds are relative to this list.
+ public Rectangle getAccessibilityItemBounds(int index, Rectangle out) {
+ if (out == null) {
+ throw new NullPointerException("out");
+ }
+ if (index < 0 || index >= size()) {
+ out.setBounds(0, 0, 0, 0);
+ return out;
+ }
+ int width = getWidth() - getStyle().getHorizontalPadding() - getSideGap();
+ calculateComponentPosition(index, width, out, getElementSize(false, true),
+ getElementSize(true, true), index <= getCurrentSelected());
+ return out;
+ }
+
+ /// Returns the accessible name produced by the list renderer for an item.
+ public String getAccessibilityItemText(int index) {
+ if (index < 0 || index >= size()) {
+ return null;
+ }
+ T value = model.getItemAt(index);
+ Component rendererComponent = renderer.getListCellRendererComponent(this, value, index,
+ index == getSelectedIndex());
+ String text = rendererComponent == null ? null : rendererComponent.getAccessibilityText();
+ if (text == null || text.length() == 0) {
+ text = value == null ? null : String.valueOf(value);
+ }
+ return text;
+ }
+
/// Returns the visual selection during a drag operation, otherwise equivalent to model.getSelectedIndex
///
/// #### Returns
@@ -515,7 +546,12 @@ public void setSelectedIndex(int index, boolean scrollToSelection) {
if (index < 0) {
throw new IllegalArgumentException("Selection index is negative:" + index);
}
+ int oldIndex = model.getSelectedIndex();
model.setSelectedIndex(index);
+ if (oldIndex != index) {
+ accessibilityChanged(com.codename1.ui.accessibility.AccessibilityManager.CHANGE_STATE
+ | com.codename1.ui.accessibility.AccessibilityManager.CHANGE_VALUE);
+ }
if (!isInitialized()) {
Form f = getComponentForm();
if (f == null) {
diff --git a/CodenameOne/src/com/codename1/ui/RadioButton.java b/CodenameOne/src/com/codename1/ui/RadioButton.java
index 0b341d68492..07cb36793ed 100644
--- a/CodenameOne/src/com/codename1/ui/RadioButton.java
+++ b/CodenameOne/src/com/codename1/ui/RadioButton.java
@@ -255,6 +255,7 @@ void setSelectedImpl(boolean selected) {
this.selected = selected;
if (changed) {
fireChangeEvent();
+ accessibilityChanged(com.codename1.ui.accessibility.AccessibilityManager.CHANGE_STATE);
}
repaint();
}
diff --git a/CodenameOne/src/com/codename1/ui/Slider.java b/CodenameOne/src/com/codename1/ui/Slider.java
index 488b0c8cf20..0ff06ff30f5 100644
--- a/CodenameOne/src/com/codename1/ui/Slider.java
+++ b/CodenameOne/src/com/codename1/ui/Slider.java
@@ -304,7 +304,11 @@ public int getProgress(ActionEvent evt) {
/// thumb sliders) can route their own input through the same value-set +
/// repaint pipeline as the built-in handlers. See #1523.
protected void setProgressInternal(int value) {
+ int oldValue = this.value;
this.value = value;
+ if (oldValue != value) {
+ accessibilityChanged(com.codename1.ui.accessibility.AccessibilityManager.CHANGE_VALUE);
+ }
if (renderValueOnTop || renderPercentageOnTop) {
super.setText(formattedValue(value));
} else {
diff --git a/CodenameOne/src/com/codename1/ui/Tabs.java b/CodenameOne/src/com/codename1/ui/Tabs.java
index e939fff6495..a43c9cf0f9d 100644
--- a/CodenameOne/src/com/codename1/ui/Tabs.java
+++ b/CodenameOne/src/com/codename1/ui/Tabs.java
@@ -1300,6 +1300,8 @@ public void setSelectedIndex(int index, boolean slideToSelected) {
if (index == activeComponent) {
return;
}
+ accessibilityChanged(com.codename1.ui.accessibility.AccessibilityManager.CHANGE_STATE
+ | com.codename1.ui.accessibility.AccessibilityManager.CHANGE_PANE);
// Snapshot the current tab bounds *before* we mutate state, so the
// animated indicator can tween from where it visibly is to the new
// selection's bounds.
diff --git a/CodenameOne/src/com/codename1/ui/TextArea.java b/CodenameOne/src/com/codename1/ui/TextArea.java
index bebfe7ec53b..aea2cdee7b8 100644
--- a/CodenameOne/src/com/codename1/ui/TextArea.java
+++ b/CodenameOne/src/com/codename1/ui/TextArea.java
@@ -609,6 +609,8 @@ && textMightGrowByContent(old, text)) {
}
if (!Objects.equals(text, old)) {
fireDataChanged(DataChangedListener.CHANGED, -1);
+ accessibilityChanged(com.codename1.ui.accessibility.AccessibilityManager.CHANGE_VALUE
+ | com.codename1.ui.accessibility.AccessibilityManager.CHANGE_CONTENT);
}
// while native editing we don't need the cursor animations
if (Display.getInstance().isNativeInputSupported() && Display.getInstance().isTextEditing(this)) {
diff --git a/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityAction.java b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityAction.java
new file mode 100644
index 00000000000..a0bb01d945d
--- /dev/null
+++ b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityAction.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (c) 2026 Codename One and contributors. All rights reserved.
+ */
+package com.codename1.ui.accessibility;
+
+import com.codename1.ui.Component;
+
+/// An action that assistive technology can invoke on a semantic node.
+public final class AccessibilityAction {
+ public static final String ACTIVATE = "activate";
+ public static final String LONG_PRESS = "longPress";
+ public static final String INCREMENT = "increment";
+ public static final String DECREMENT = "decrement";
+ public static final String SET_VALUE = "setValue";
+ public static final String SET_TEXT = "setText";
+ public static final String FOCUS = "focus";
+ public static final String DISMISS = "dismiss";
+ public static final String EXPAND = "expand";
+ public static final String COLLAPSE = "collapse";
+ public static final String SCROLL_FORWARD = "scrollForward";
+ public static final String SCROLL_BACKWARD = "scrollBackward";
+ public static final String COPY = "copy";
+ public static final String CUT = "cut";
+ public static final String PASTE = "paste";
+ public static final String SHOW_ON_SCREEN = "showOnScreen";
+ public static final String SET_SELECTION = "setSelection";
+ public static final String MOVE_CURSOR_FORWARD_BY_CHARACTER = "moveCursorForwardByCharacter";
+ public static final String MOVE_CURSOR_BACKWARD_BY_CHARACTER = "moveCursorBackwardByCharacter";
+ public static final String MOVE_CURSOR_FORWARD_BY_WORD = "moveCursorForwardByWord";
+ public static final String MOVE_CURSOR_BACKWARD_BY_WORD = "moveCursorBackwardByWord";
+
+ /// Handles an accessibility action on the Codename One EDT.
+ public interface Handler {
+ boolean perform(Component component, Object argument);
+ }
+
+ private final String id;
+ private final String label;
+ private final Handler handler;
+ private final boolean enabled;
+
+ public AccessibilityAction(String id, String label, Handler handler) {
+ this(id, label, handler, true);
+ }
+
+ public AccessibilityAction(String id, String label, Handler handler, boolean enabled) {
+ if (id == null || id.length() == 0) {
+ throw new IllegalArgumentException("Accessibility action id must not be empty");
+ }
+ this.id = id;
+ this.label = label;
+ this.handler = handler;
+ this.enabled = enabled;
+ }
+
+ public String getId() { return id; }
+ public String getLabel() { return label; }
+ public boolean isEnabled() { return enabled; }
+
+ public boolean perform(Component component, Object argument) {
+ return enabled && handler != null && handler.perform(component, argument);
+ }
+}
diff --git a/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityAssertions.java b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityAssertions.java
new file mode 100644
index 00000000000..139dab70908
--- /dev/null
+++ b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityAssertions.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright (c) 2026 Codename One and contributors. All rights reserved.
+ */
+package com.codename1.ui.accessibility;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/// Automated semantic-tree assertions suitable for unit tests and CI.
+public final class AccessibilityAssertions {
+ private AccessibilityAssertions() {
+ }
+
+ public static List audit(AccessibilityTreeSnapshot tree) {
+ List issues = new ArrayList();
+ Set identifiers = new HashSet();
+ for (AccessibilityNodeSnapshot node : tree.getNodes().values()) {
+ auditNode(tree, node, identifiers, issues);
+ }
+ for (Long root : tree.getRootIds()) {
+ detectCycle(tree, root.longValue(), new HashSet(), new HashSet(), issues);
+ }
+ return issues;
+ }
+
+ public static void assertNoErrors(AccessibilityTreeSnapshot tree) {
+ List issues = audit(tree);
+ StringBuilder errors = new StringBuilder();
+ for (AccessibilityIssue issue : issues) {
+ if (issue.getSeverity() == AccessibilityIssue.Severity.ERROR) {
+ if (errors.length() > 0) errors.append('\n');
+ errors.append(issue.toString());
+ }
+ }
+ if (errors.length() > 0) throw new AssertionError(errors.toString());
+ }
+
+ public static void assertNoUnlabeledInteractiveNodes(AccessibilityTreeSnapshot tree) {
+ StringBuilder errors = new StringBuilder();
+ for (AccessibilityNodeSnapshot node : tree.getNodes().values()) {
+ if (node.isFocusable() && empty(node.getLabel()) && empty(node.getValue())) {
+ if (errors.length() > 0) errors.append('\n');
+ errors.append("Interactive node ").append(node.getId()).append(" (")
+ .append(node.getRole()).append(") has no accessible name");
+ }
+ }
+ if (errors.length() > 0) throw new AssertionError(errors.toString());
+ }
+
+ private static void auditNode(AccessibilityTreeSnapshot tree, AccessibilityNodeSnapshot node,
+ Set identifiers, List issues) {
+ if (node.isFocusable() && empty(node.getLabel()) && empty(node.getValue())) {
+ error(issues, "unlabeled-interactive", "Interactive nodes require a label or value", node);
+ }
+ if (node.getRange() != null && !node.getRange().isValid()) {
+ error(issues, "invalid-range", "Range must satisfy min <= current <= max and step >= 0", node);
+ }
+ if (node.getRole() == AccessibilityRole.HEADING && node.getHeadingLevel() == 0) {
+ error(issues, "invalid-heading-level", "Heading levels are 1-based", node);
+ }
+ if (node.getChecked() != AccessibilityCheckedState.UNSPECIFIED
+ && node.getRole() != AccessibilityRole.CHECKBOX
+ && node.getRole() != AccessibilityRole.RADIO_BUTTON
+ && node.getRole() != AccessibilityRole.SWITCH
+ && node.getRole() != AccessibilityRole.TREE_ITEM
+ && node.getRole() != AccessibilityRole.MENU_ITEM) {
+ warning(issues, "checked-role", "Checked state is unusual for this role", node);
+ }
+ if (node.getExpanded() != null && node.getRole() != AccessibilityRole.TREE_ITEM
+ && node.getRole() != AccessibilityRole.BUTTON && node.getRole() != AccessibilityRole.MENU_ITEM
+ && node.getRole() != AccessibilityRole.COMBO_BOX && node.getRole() != AccessibilityRole.GENERIC) {
+ warning(issues, "expanded-role", "Expanded state is unusual for this role", node);
+ }
+ if (node.getCollectionItemInfo() != null) {
+ AccessibilityCollectionItemInfo item = node.getCollectionItemInfo();
+ if (item.getPositionInSet() == 0 || item.getSetSize() == 0
+ || item.getPositionInSet() > 0 && item.getSetSize() > 0
+ && item.getPositionInSet() > item.getSetSize()) {
+ error(issues, "invalid-collection-position", "Collection position and size must be 1-based and consistent", node);
+ }
+ }
+ if (node.getParentId() >= 0 && tree.getNode(node.getParentId()) == null) {
+ error(issues, "missing-parent", "Semantic parent does not exist", node);
+ }
+ if (node.getIdentifier() != null && !identifiers.add(node.getIdentifier())) {
+ error(issues, "duplicate-identifier", "Accessibility identifiers must be unique", node);
+ }
+ if (node.getBounds().getWidth() <= 0 || node.getBounds().getHeight() <= 0) {
+ warning(issues, "empty-bounds", "Node is fully clipped or has empty bounds", node);
+ }
+ Set actions = new HashSet();
+ for (AccessibilityAction action : node.getActions()) {
+ if (!actions.add(action.getId())) error(issues, "duplicate-action", "Action ids must be unique per node", node);
+ if (!isStandardAction(action.getId()) && empty(action.getLabel())) {
+ error(issues, "unlabeled-custom-action", "Custom accessibility actions require localized labels", node);
+ }
+ }
+ if ((node.getRole() == AccessibilityRole.DIALOG || node.getRole() == AccessibilityRole.ALERT)
+ && empty(node.getLabel()) && empty(node.getPaneTitle())) {
+ error(issues, "unnamed-dialog", "Dialogs require a label or pane title", node);
+ }
+ }
+
+ private static void detectCycle(AccessibilityTreeSnapshot tree, long id, Set visiting,
+ Set visited, List issues) {
+ Long boxed = Long.valueOf(id);
+ if (visited.contains(boxed)) return;
+ if (!visiting.add(boxed)) {
+ AccessibilityNodeSnapshot node = tree.getNode(id);
+ if (node != null) error(issues, "tree-cycle", "Semantic tree contains a cycle", node);
+ return;
+ }
+ AccessibilityNodeSnapshot node = tree.getNode(id);
+ if (node != null) {
+ for (Long child : node.getChildIds()) detectCycle(tree, child.longValue(), visiting, visited, issues);
+ }
+ visiting.remove(boxed);
+ visited.add(boxed);
+ }
+
+ private static boolean isStandardAction(String id) {
+ return AccessibilityAction.ACTIVATE.equals(id) || AccessibilityAction.LONG_PRESS.equals(id)
+ || AccessibilityAction.INCREMENT.equals(id) || AccessibilityAction.DECREMENT.equals(id)
+ || AccessibilityAction.SET_VALUE.equals(id) || AccessibilityAction.SET_TEXT.equals(id)
+ || AccessibilityAction.FOCUS.equals(id) || AccessibilityAction.DISMISS.equals(id)
+ || AccessibilityAction.EXPAND.equals(id) || AccessibilityAction.COLLAPSE.equals(id)
+ || AccessibilityAction.SCROLL_FORWARD.equals(id) || AccessibilityAction.SCROLL_BACKWARD.equals(id)
+ || AccessibilityAction.COPY.equals(id) || AccessibilityAction.CUT.equals(id)
+ || AccessibilityAction.PASTE.equals(id) || AccessibilityAction.SHOW_ON_SCREEN.equals(id)
+ || AccessibilityAction.SET_SELECTION.equals(id)
+ || AccessibilityAction.MOVE_CURSOR_FORWARD_BY_CHARACTER.equals(id)
+ || AccessibilityAction.MOVE_CURSOR_BACKWARD_BY_CHARACTER.equals(id)
+ || AccessibilityAction.MOVE_CURSOR_FORWARD_BY_WORD.equals(id)
+ || AccessibilityAction.MOVE_CURSOR_BACKWARD_BY_WORD.equals(id);
+ }
+
+ private static void error(List issues, String code, String message,
+ AccessibilityNodeSnapshot node) {
+ issues.add(new AccessibilityIssue(AccessibilityIssue.Severity.ERROR, code, message, node.getId()));
+ }
+
+ private static void warning(List issues, String code, String message,
+ AccessibilityNodeSnapshot node) {
+ issues.add(new AccessibilityIssue(AccessibilityIssue.Severity.WARNING, code, message, node.getId()));
+ }
+
+ private static boolean empty(String value) { return value == null || value.length() == 0; }
+}
diff --git a/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityCheckedState.java b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityCheckedState.java
new file mode 100644
index 00000000000..4b99c355b7a
--- /dev/null
+++ b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityCheckedState.java
@@ -0,0 +1,12 @@
+/*
+ * Copyright (c) 2026 Codename One and contributors. All rights reserved.
+ */
+package com.codename1.ui.accessibility;
+
+/// Checked state for checkable semantic nodes.
+public enum AccessibilityCheckedState {
+ UNSPECIFIED,
+ UNCHECKED,
+ CHECKED,
+ MIXED
+}
diff --git a/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityChildProvider.java b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityChildProvider.java
new file mode 100644
index 00000000000..a4cea03e026
--- /dev/null
+++ b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityChildProvider.java
@@ -0,0 +1,13 @@
+/*
+ * Copyright (c) 2026 Codename One and contributors. All rights reserved.
+ */
+package com.codename1.ui.accessibility;
+
+import com.codename1.ui.Component;
+import java.util.List;
+
+/// Supplies semantic children that are not represented by persistent lightweight
+/// components, such as renderer-backed list rows and table cells.
+public interface AccessibilityChildProvider {
+ List getAccessibilityChildren(Component host);
+}
diff --git a/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityCollectionInfo.java b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityCollectionInfo.java
new file mode 100644
index 00000000000..e5d4b221a38
--- /dev/null
+++ b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityCollectionInfo.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (c) 2026 Codename One and contributors. All rights reserved.
+ */
+package com.codename1.ui.accessibility;
+
+/// Metadata describing a list, grid, table, or other collection.
+public final class AccessibilityCollectionInfo {
+ public static final int SELECTION_NONE = 0;
+ public static final int SELECTION_SINGLE = 1;
+ public static final int SELECTION_MULTIPLE = 2;
+
+ private final int rowCount;
+ private final int columnCount;
+ private final boolean hierarchical;
+ private final int selectionMode;
+
+ public AccessibilityCollectionInfo(int rowCount, int columnCount) {
+ this(rowCount, columnCount, false, SELECTION_NONE);
+ }
+
+ public AccessibilityCollectionInfo(int rowCount, int columnCount, boolean hierarchical, int selectionMode) {
+ this.rowCount = rowCount;
+ this.columnCount = columnCount;
+ this.hierarchical = hierarchical;
+ this.selectionMode = selectionMode;
+ }
+
+ public int getRowCount() {
+ return rowCount;
+ }
+
+ public int getColumnCount() {
+ return columnCount;
+ }
+
+ public boolean isHierarchical() {
+ return hierarchical;
+ }
+
+ public int getSelectionMode() {
+ return selectionMode;
+ }
+}
diff --git a/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityCollectionItemInfo.java b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityCollectionItemInfo.java
new file mode 100644
index 00000000000..0ea48ccfbbc
--- /dev/null
+++ b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityCollectionItemInfo.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (c) 2026 Codename One and contributors. All rights reserved.
+ */
+package com.codename1.ui.accessibility;
+
+/// Position and span metadata for an item in a semantic collection.
+public final class AccessibilityCollectionItemInfo {
+ private final int rowIndex;
+ private final int rowSpan;
+ private final int columnIndex;
+ private final int columnSpan;
+ private final int positionInSet;
+ private final int setSize;
+ private final int level;
+ private final boolean heading;
+
+ public AccessibilityCollectionItemInfo(int rowIndex, int columnIndex) {
+ this(rowIndex, 1, columnIndex, 1, -1, -1, -1, false);
+ }
+
+ public AccessibilityCollectionItemInfo(int rowIndex, int rowSpan, int columnIndex, int columnSpan,
+ int positionInSet, int setSize, int level, boolean heading) {
+ this.rowIndex = rowIndex;
+ this.rowSpan = rowSpan;
+ this.columnIndex = columnIndex;
+ this.columnSpan = columnSpan;
+ this.positionInSet = positionInSet;
+ this.setSize = setSize;
+ this.level = level;
+ this.heading = heading;
+ }
+
+ public int getRowIndex() { return rowIndex; }
+ public int getRowSpan() { return rowSpan; }
+ public int getColumnIndex() { return columnIndex; }
+ public int getColumnSpan() { return columnSpan; }
+ public int getPositionInSet() { return positionInSet; }
+ public int getSetSize() { return setSize; }
+ public int getLevel() { return level; }
+ public boolean isHeading() { return heading; }
+}
diff --git a/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityGrouping.java b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityGrouping.java
new file mode 100644
index 00000000000..96c767ea489
--- /dev/null
+++ b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityGrouping.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright (c) 2026 Codename One and contributors. All rights reserved.
+ */
+package com.codename1.ui.accessibility;
+
+/// Controls how a component and its descendants participate in the semantic tree.
+public enum AccessibilityGrouping {
+ /// Infer exposure from the role, content, actions, and component type.
+ AUTO,
+ /// Expose this node and omit all semantic descendants.
+ LEAF,
+ /// Expose this node and its semantic descendants.
+ GROUP,
+ /// Expose a single node whose label and description include its descendants.
+ MERGE_DESCENDANTS,
+ /// Omit this node but promote its semantic children.
+ EXCLUDE,
+ /// Omit this node and its entire semantic subtree.
+ EXCLUDE_SUBTREE
+}
diff --git a/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityInspector.java b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityInspector.java
new file mode 100644
index 00000000000..cea6ae8c0fb
--- /dev/null
+++ b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityInspector.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright (c) 2026 Codename One and contributors. All rights reserved.
+ */
+package com.codename1.ui.accessibility;
+
+import com.codename1.ui.Form;
+
+/// Read-only semantic-tree inspection entry point for tests and developer tools.
+public final class AccessibilityInspector {
+ private AccessibilityInspector() {
+ }
+
+ public static AccessibilityTreeSnapshot snapshot(Form form) {
+ return AccessibilityManager.getInstance().getSnapshot(form);
+ }
+
+ public static AccessibilityTreeSnapshot currentSnapshot() {
+ return AccessibilityManager.getInstance().getCurrentSnapshot();
+ }
+}
diff --git a/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityIssue.java b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityIssue.java
new file mode 100644
index 00000000000..6251e00703b
--- /dev/null
+++ b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityIssue.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (c) 2026 Codename One and contributors. All rights reserved.
+ */
+package com.codename1.ui.accessibility;
+
+/// A deterministic accessibility audit finding.
+public final class AccessibilityIssue {
+ public enum Severity { ERROR, WARNING }
+
+ private final Severity severity;
+ private final String code;
+ private final String message;
+ private final long nodeId;
+
+ public AccessibilityIssue(Severity severity, String code, String message, long nodeId) {
+ this.severity = severity;
+ this.code = code;
+ this.message = message;
+ this.nodeId = nodeId;
+ }
+
+ public Severity getSeverity() { return severity; }
+ public String getCode() { return code; }
+ public String getMessage() { return message; }
+ public long getNodeId() { return nodeId; }
+
+ @Override
+ public String toString() {
+ return severity + " " + code + " [node " + nodeId + "]: " + message;
+ }
+}
diff --git a/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityLiveRegion.java b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityLiveRegion.java
new file mode 100644
index 00000000000..46ac6b73350
--- /dev/null
+++ b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityLiveRegion.java
@@ -0,0 +1,11 @@
+/*
+ * Copyright (c) 2026 Codename One and contributors. All rights reserved.
+ */
+package com.codename1.ui.accessibility;
+
+/// How changes to a semantic node should be announced without moving focus.
+public enum AccessibilityLiveRegion {
+ OFF,
+ POLITE,
+ ASSERTIVE
+}
diff --git a/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityManager.java b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityManager.java
new file mode 100644
index 00000000000..476e826c801
--- /dev/null
+++ b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityManager.java
@@ -0,0 +1,669 @@
+/*
+ * Copyright (c) 2026 Codename One and contributors. All rights reserved.
+ */
+package com.codename1.ui.accessibility;
+
+import com.codename1.ui.Button;
+import com.codename1.ui.CheckBox;
+import com.codename1.ui.Component;
+import com.codename1.ui.Container;
+import com.codename1.ui.Dialog;
+import com.codename1.ui.Display;
+import com.codename1.ui.Form;
+import com.codename1.ui.Label;
+import com.codename1.ui.RadioButton;
+import com.codename1.ui.Slider;
+import com.codename1.ui.Tabs;
+import com.codename1.ui.TextArea;
+import com.codename1.ui.TextField;
+import com.codename1.ui.geom.Rectangle;
+import com.codename1.ui.table.Table;
+import com.codename1.ui.util.WeakHashMap;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/// Builds, caches, diffs, and dispatches actions for the portable semantic tree.
+///
+/// The tree is always constructed on the Codename One EDT. Native ports read the
+/// last immutable snapshot and post actions through {@link #performAction(long, String, Object)},
+/// so native accessibility threads never synchronously enter application code.
+public final class AccessibilityManager {
+ public static final int CHANGE_STRUCTURE = 1;
+ public static final int CHANGE_CONTENT = 2;
+ public static final int CHANGE_STATE = 4;
+ public static final int CHANGE_VALUE = 8;
+ public static final int CHANGE_BOUNDS = 16;
+ public static final int CHANGE_FOCUS = 32;
+ public static final int CHANGE_ACTIONS = 64;
+ public static final int CHANGE_LIVE_REGION = 128;
+ public static final int CHANGE_PANE = 256;
+ public static final int CHANGE_ALL = 0x7fffffff;
+
+ private static final AccessibilityManager INSTANCE = new AccessibilityManager();
+ private final Map componentIds = new WeakHashMap();
+ private final Map virtualIds = new HashMap();
+ private long nextId = 1;
+ private long generation;
+ private boolean dirty = true;
+ private boolean refreshScheduled;
+ private int pendingChanges = CHANGE_ALL;
+ private Form snapshotForm;
+ private AccessibilityTreeSnapshot snapshot = new AccessibilityTreeSnapshot(0,
+ Collections.emptyList(), Collections.emptyMap());
+
+ private AccessibilityManager() {
+ }
+
+ public static AccessibilityManager getInstance() {
+ return INSTANCE;
+ }
+
+ public synchronized void invalidate(Component component, int changeType) {
+ dirty = true;
+ pendingChanges |= changeType;
+ try {
+ if (!refreshScheduled) {
+ refreshScheduled = true;
+ Display.getInstance().callSerially(new Runnable() {
+ public void run() {
+ int changes;
+ synchronized (AccessibilityManager.this) {
+ changes = pendingChanges;
+ }
+ getSnapshot(Display.getInstance().getCurrent());
+ synchronized (AccessibilityManager.this) {
+ refreshScheduled = false;
+ }
+ Display.getInstance().accessibilityTreeChanged(changes);
+ }
+ });
+ }
+ } catch (Throwable ignored) {
+ // Display may not be initialized yet while an application constructs its first form.
+ refreshScheduled = false;
+ }
+ }
+
+ public void invalidateAll() {
+ invalidate(null, CHANGE_ALL);
+ }
+
+ public AccessibilityTreeSnapshot getCurrentSnapshot() {
+ synchronized (this) {
+ if (dirty && !Display.getInstance().isEdt()) return snapshot;
+ }
+ return getSnapshot(Display.getInstance().getCurrent());
+ }
+
+ public synchronized AccessibilityTreeSnapshot getSnapshot(Form form) {
+ if (!dirty && form == snapshotForm) return snapshot;
+ if (form == null) {
+ snapshot = new AccessibilityTreeSnapshot(++generation, Collections.emptyList(),
+ Collections.emptyMap());
+ snapshotForm = null;
+ dirty = false;
+ pendingChanges = 0;
+ return snapshot;
+ }
+
+ List roots = new ArrayList();
+ resolveComponent(form, roots);
+ sortTree(roots);
+ List rootIds = new ArrayList();
+ LinkedHashMap nodes = new LinkedHashMap();
+ freeze(roots, -1, rootIds, nodes);
+ snapshot = new AccessibilityTreeSnapshot(++generation, rootIds, nodes);
+ snapshotForm = form;
+ dirty = false;
+ pendingChanges = 0;
+ return snapshot;
+ }
+
+ public synchronized int getPendingChanges() {
+ return pendingChanges;
+ }
+
+ public boolean performAction(long nodeId, String actionId, final Object argument) {
+ final AccessibilityNodeSnapshot node;
+ final AccessibilityAction action;
+ synchronized (this) {
+ node = snapshot.getNode(nodeId);
+ action = node == null ? null : node.getAction(actionId);
+ }
+ if (node == null || action == null || !action.isEnabled()) return false;
+ Display.getInstance().callSerially(new Runnable() {
+ public void run() {
+ action.perform(node.getComponent(), argument);
+ invalidate(node.getComponent(), CHANGE_STATE | CHANGE_VALUE | CHANGE_CONTENT);
+ }
+ });
+ return true;
+ }
+
+ /// Dispatches an action selected by a native bridge that can transport only an integer token.
+ public boolean performActionByHash(long nodeId, int actionIdHash, Object argument) {
+ AccessibilityNodeSnapshot node;
+ synchronized (this) {
+ node = snapshot.getNode(nodeId);
+ }
+ if (node == null) return false;
+ for (AccessibilityAction action : node.getActions()) {
+ if (action.getId().hashCode() == actionIdHash) {
+ return performAction(nodeId, action.getId(), argument);
+ }
+ }
+ return false;
+ }
+
+ private long idFor(Component component) {
+ Long id = componentIds.get(component);
+ if (id == null) {
+ id = Long.valueOf(nextId++);
+ componentIds.put(component, id);
+ }
+ return id.longValue();
+ }
+
+ private long idForVirtual(Component host, String path) {
+ String key = idFor(host) + ":" + path;
+ Long id = virtualIds.get(key);
+ if (id == null) {
+ id = Long.valueOf(nextId++);
+ virtualIds.put(key, id);
+ }
+ return id.longValue();
+ }
+
+ private void resolveComponent(Component component, List destination) {
+ if (component == null || (!(component instanceof Form) && !component.isVisible())
+ || component.isHidden(true)) return;
+ AccessibilityNode config = component.getSemantics();
+ AccessibilityGrouping grouping = config.getGrouping();
+ if (grouping == AccessibilityGrouping.EXCLUDE_SUBTREE) return;
+
+ BuildNode node = buildComponentNode(component, config);
+ List children = new ArrayList();
+ if (grouping != AccessibilityGrouping.LEAF && component instanceof Container) {
+ Container container = (Container)component;
+ for (int i = 0; i < container.getComponentCount(); i++) {
+ resolveComponent(container.getComponentAt(i), children);
+ }
+ }
+ if (grouping != AccessibilityGrouping.LEAF) {
+ addVirtualChildren(component, config, node.id, "custom", children);
+ if (component instanceof com.codename1.ui.List) {
+ addListChildren((com.codename1.ui.List)component, node.id, children);
+ }
+ }
+
+ boolean expose = shouldExpose(component, config, node);
+ if (grouping == AccessibilityGrouping.EXCLUDE) expose = false;
+ if (grouping == AccessibilityGrouping.MERGE_DESCENDANTS) {
+ String merged = collectLabels(children);
+ if (isEmpty(node.builder.label)) node.builder.label = merged;
+ else if (!isEmpty(merged)) node.builder.description = join(node.builder.description, merged);
+ children.clear();
+ expose = true;
+ }
+ if (grouping == AccessibilityGrouping.LEAF || grouping == AccessibilityGrouping.GROUP) expose = true;
+
+ if (expose) {
+ node.children.addAll(children);
+ destination.add(node);
+ } else {
+ destination.addAll(children);
+ }
+ }
+
+ private BuildNode buildComponentNode(final Component component, AccessibilityNode config) {
+ BuildNode out = new BuildNode();
+ out.id = idFor(component);
+ out.builder.id = out.id;
+ out.builder.component = component;
+ out.builder.identifier = config.getIdentifier();
+ out.builder.label = firstNonEmpty(config.getLabel(), component.getAccessibilityText());
+ out.builder.hint = config.getHint();
+ out.builder.description = config.getDescription();
+ out.builder.value = config.getValue();
+ out.builder.validationError = config.getValidationError();
+ out.builder.paneTitle = config.getPaneTitle();
+ out.builder.roleDescription = config.getRoleDescription();
+ out.builder.role = config.getRole() == AccessibilityRole.NONE ? inferRole(component) : config.getRole();
+ out.builder.checked = inferChecked(component, config);
+ out.builder.liveRegion = config.getLiveRegion();
+ out.builder.range = config.getRange() == null ? inferRange(component) : config.getRange();
+ out.builder.collectionInfo = config.getCollectionInfo() == null ? inferCollection(component) : config.getCollectionInfo();
+ out.builder.collectionItemInfo = config.getCollectionItemInfo() == null
+ ? inferCollectionItem(component) : config.getCollectionItemInfo();
+ out.builder.bounds = componentBounds(component);
+ out.builder.selected = config.getSelected() == null ? inferSelected(component) : config.getSelected();
+ out.builder.expanded = config.getExpanded();
+ out.builder.enabled = config.getEnabled() == null ? Boolean.valueOf(component.isEnabled()) : config.getEnabled();
+ out.builder.invalid = config.getInvalid();
+ out.builder.busy = config.getBusy();
+ out.builder.readOnly = config.getReadOnly() == null ? inferReadOnly(component) : config.getReadOnly();
+ out.builder.required = config.getRequired();
+ out.builder.multiline = config.getMultiline() == null ? inferMultiline(component) : config.getMultiline();
+ out.builder.obscured = config.getObscured() == null ? inferObscured(component) : config.getObscured();
+ out.builder.pressed = config.getPressed();
+ out.builder.current = config.getCurrent();
+ out.builder.modal = config.isModal() || component instanceof Dialog;
+ out.builder.focusable = component.isFocusable() || isInteractiveRole(out.builder.role);
+ out.builder.focused = component.hasFocus();
+ out.builder.headingLevel = config.getHeadingLevel();
+ out.builder.sortKey = config.getSortKey();
+ out.builder.traversalBefore = config.getTraversalBefore();
+ out.builder.traversalAfter = config.getTraversalAfter();
+ out.builder.actions.addAll(config.getActions());
+ addDefaultActions(component, out.builder);
+ if (component instanceof Form && isEmpty(out.builder.paneTitle)) {
+ out.builder.paneTitle = ((Form)component).getTitle();
+ }
+ out.component = component;
+ return out;
+ }
+
+ private void addVirtualChildren(Component host, AccessibilityNode config, long hostId,
+ String path, List destination) {
+ List virtual = new ArrayList();
+ virtual.addAll(config.getChildren());
+ if (config.getChildProvider() != null) {
+ List provided = config.getChildProvider().getAccessibilityChildren(host);
+ if (provided != null) virtual.addAll(provided);
+ }
+ for (int i = 0; i < virtual.size(); i++) {
+ AccessibilityNode child = virtual.get(i);
+ String key = child.getVirtualKey();
+ if (isEmpty(key)) key = "index-" + i;
+ BuildNode resolved = buildVirtualNode(host, child, path + "/" + key);
+ addVirtualChildren(host, child, resolved.id, path + "/" + key, resolved.children);
+ destination.add(resolved);
+ }
+ }
+
+ private BuildNode buildVirtualNode(Component host, AccessibilityNode config, String path) {
+ BuildNode out = new BuildNode();
+ out.id = idForVirtual(host, path);
+ out.builder.id = out.id;
+ out.builder.component = host;
+ out.builder.virtualKey = config.getVirtualKey();
+ out.builder.identifier = config.getIdentifier();
+ out.builder.label = config.getLabel();
+ out.builder.hint = config.getHint();
+ out.builder.description = config.getDescription();
+ out.builder.value = config.getValue();
+ out.builder.validationError = config.getValidationError();
+ out.builder.paneTitle = config.getPaneTitle();
+ out.builder.roleDescription = config.getRoleDescription();
+ out.builder.role = config.getRole() == AccessibilityRole.NONE ? AccessibilityRole.GENERIC : config.getRole();
+ out.builder.checked = config.getChecked();
+ out.builder.liveRegion = config.getLiveRegion();
+ out.builder.range = config.getRange();
+ out.builder.collectionInfo = config.getCollectionInfo();
+ out.builder.collectionItemInfo = config.getCollectionItemInfo();
+ out.builder.bounds = virtualBounds(host, config.getBounds());
+ out.builder.selected = config.getSelected();
+ out.builder.expanded = config.getExpanded();
+ out.builder.enabled = config.getEnabled() == null ? Boolean.valueOf(host.isEnabled()) : config.getEnabled();
+ out.builder.invalid = config.getInvalid();
+ out.builder.busy = config.getBusy();
+ out.builder.readOnly = config.getReadOnly();
+ out.builder.required = config.getRequired();
+ out.builder.multiline = config.getMultiline();
+ out.builder.obscured = config.getObscured();
+ out.builder.pressed = config.getPressed();
+ out.builder.current = config.getCurrent();
+ out.builder.modal = config.isModal();
+ out.builder.focusable = !config.getActions().isEmpty() || isInteractiveRole(out.builder.role);
+ out.builder.headingLevel = config.getHeadingLevel();
+ out.builder.sortKey = config.getSortKey();
+ out.builder.actions.addAll(config.getActions());
+ out.component = host;
+ return out;
+ }
+
+ private void addListChildren(final com.codename1.ui.List list, long hostId, List destination) {
+ int size = list.size();
+ for (int i = 0; i < size; i++) {
+ final int index = i;
+ AccessibilityNode item = new AccessibilityNode("item-" + i)
+ .setRole(AccessibilityRole.LIST_ITEM)
+ .setLabel(list.getAccessibilityItemText(i))
+ .setSelected(Boolean.valueOf(i == list.getSelectedIndex()))
+ .setCollectionItemInfo(new AccessibilityCollectionItemInfo(i, 1, 0, 1,
+ i + 1, size, 1, false))
+ .setBounds(list.getAccessibilityItemBounds(i, new Rectangle()))
+ .addAction(new AccessibilityAction(AccessibilityAction.ACTIVATE, null,
+ new AccessibilityAction.Handler() {
+ public boolean perform(Component component, Object argument) {
+ if (!list.isEnabled()) return false;
+ list.setSelectedIndex(index);
+ list.keyReleased(Display.getInstance().getKeyCode(Display.GAME_FIRE));
+ return true;
+ }
+ }));
+ destination.add(buildVirtualNode(list, item, "list/item-" + i));
+ }
+ }
+
+ private void addDefaultActions(final Component component, AccessibilityNodeSnapshot.Builder builder) {
+ if (component.isFocusable() && !hasAction(builder.actions, AccessibilityAction.FOCUS)) {
+ builder.actions.add(new AccessibilityAction(AccessibilityAction.FOCUS, null,
+ new AccessibilityAction.Handler() {
+ public boolean perform(Component source, Object argument) {
+ if (!source.isEnabled()) return false;
+ source.requestFocus();
+ return true;
+ }
+ }));
+ }
+ if (component instanceof Button && !hasAction(builder.actions, AccessibilityAction.ACTIVATE)) {
+ builder.actions.add(new AccessibilityAction(AccessibilityAction.ACTIVATE, null,
+ new AccessibilityAction.Handler() {
+ public boolean perform(Component source, Object argument) {
+ if (!source.isEnabled()) return false;
+ source.keyReleased(Display.getInstance().getKeyCode(Display.GAME_FIRE));
+ return true;
+ }
+ }));
+ }
+ if (component instanceof Slider) {
+ final Slider slider = (Slider)component;
+ if (slider.isEditable() && !hasAction(builder.actions, AccessibilityAction.INCREMENT)) {
+ builder.actions.add(new AccessibilityAction(AccessibilityAction.INCREMENT, null,
+ new AccessibilityAction.Handler() {
+ public boolean perform(Component source, Object argument) {
+ slider.setProgress(Math.min(slider.getMaxValue(), slider.getProgress() + Math.max(1, slider.getIncrements())));
+ return true;
+ }
+ }));
+ }
+ if (slider.isEditable() && !hasAction(builder.actions, AccessibilityAction.DECREMENT)) {
+ builder.actions.add(new AccessibilityAction(AccessibilityAction.DECREMENT, null,
+ new AccessibilityAction.Handler() {
+ public boolean perform(Component source, Object argument) {
+ slider.setProgress(Math.max(slider.getMinValue(), slider.getProgress() - Math.max(1, slider.getIncrements())));
+ return true;
+ }
+ }));
+ }
+ }
+ if (component instanceof TextArea) {
+ final TextArea text = (TextArea)component;
+ if (!hasAction(builder.actions, AccessibilityAction.FOCUS)) {
+ builder.actions.add(new AccessibilityAction(AccessibilityAction.FOCUS, null,
+ new AccessibilityAction.Handler() {
+ public boolean perform(Component source, Object argument) {
+ source.requestFocus();
+ return true;
+ }
+ }));
+ }
+ if (text.isEditable() && !hasAction(builder.actions, AccessibilityAction.SET_TEXT)) {
+ builder.actions.add(new AccessibilityAction(AccessibilityAction.SET_TEXT, null,
+ new AccessibilityAction.Handler() {
+ public boolean perform(Component source, Object argument) {
+ if (!(argument instanceof String)) return false;
+ text.setText((String)argument);
+ return true;
+ }
+ }));
+ }
+ }
+ }
+
+ private AccessibilityRole inferRole(Component component) {
+ if (component.getParent() instanceof Table) {
+ return ((Table)component.getParent()).getCellRow(component) < 0
+ ? AccessibilityRole.COLUMN_HEADER : AccessibilityRole.CELL;
+ }
+ Tabs tabOwner = tabPanelOwner(component);
+ if (tabOwner != null) return AccessibilityRole.TAB_PANEL;
+ if (component instanceof Dialog) return AccessibilityRole.DIALOG;
+ if (component instanceof RadioButton) return AccessibilityRole.RADIO_BUTTON;
+ if (component instanceof CheckBox) return AccessibilityRole.CHECKBOX;
+ if (component instanceof Button) {
+ if (isTabButton((Button)component)) return AccessibilityRole.TAB;
+ return ((Button)component).isToggle() ? AccessibilityRole.TOGGLE_BUTTON : AccessibilityRole.BUTTON;
+ }
+ if (component instanceof Slider) return ((Slider)component).isEditable() ? AccessibilityRole.SLIDER : AccessibilityRole.PROGRESS_BAR;
+ if (component instanceof TextField) return AccessibilityRole.TEXT_FIELD;
+ if (component instanceof TextArea) return AccessibilityRole.TEXT_FIELD;
+ if (component instanceof com.codename1.ui.List) return AccessibilityRole.LIST;
+ if (component instanceof Table) return AccessibilityRole.GRID;
+ if (component instanceof Tabs) return AccessibilityRole.TAB_LIST;
+ if (component instanceof Form) return AccessibilityRole.GENERIC;
+ if (component instanceof Label) return AccessibilityRole.STATIC_TEXT;
+ return AccessibilityRole.NONE;
+ }
+
+ private boolean isTabButton(Button button) {
+ Container parent = button.getParent();
+ while (parent != null) {
+ Container owner = parent.getParent();
+ if (owner instanceof Tabs && ((Tabs)owner).getTabsContainer() == parent) return true;
+ parent = parent.getParent();
+ }
+ return false;
+ }
+
+ private AccessibilityCheckedState inferChecked(Component component, AccessibilityNode config) {
+ if (config.getChecked() != AccessibilityCheckedState.UNSPECIFIED) return config.getChecked();
+ if (component instanceof CheckBox || component instanceof RadioButton) {
+ return ((Button)component).isSelected() ? AccessibilityCheckedState.CHECKED : AccessibilityCheckedState.UNCHECKED;
+ }
+ return AccessibilityCheckedState.UNSPECIFIED;
+ }
+
+ private Boolean inferSelected(Component component) {
+ if (component instanceof Button && (((Button)component).isToggle() || isTabButton((Button)component))) {
+ return Boolean.valueOf(((Button)component).isSelected());
+ }
+ Tabs tabOwner = tabPanelOwner(component);
+ if (tabOwner != null) return Boolean.valueOf(tabOwner.getSelectedComponent() == component);
+ return null;
+ }
+
+ private Tabs tabPanelOwner(Component component) {
+ Container parent = component.getParent();
+ if (parent != null && parent.getParent() instanceof Tabs) {
+ Tabs tabs = (Tabs)parent.getParent();
+ if (tabs.getContentPane() == parent) return tabs;
+ }
+ return null;
+ }
+
+ private AccessibilityRange inferRange(Component component) {
+ if (component instanceof Slider) {
+ Slider slider = (Slider)component;
+ return new AccessibilityRange(slider.getMinValue(), slider.getMaxValue(), slider.getProgress(), slider.getIncrements(), null);
+ }
+ return null;
+ }
+
+ private AccessibilityCollectionInfo inferCollection(Component component) {
+ if (component instanceof com.codename1.ui.List) {
+ return new AccessibilityCollectionInfo(((com.codename1.ui.List)component).size(), 1,
+ false, AccessibilityCollectionInfo.SELECTION_SINGLE);
+ }
+ if (component instanceof Table) {
+ Table table = (Table)component;
+ return new AccessibilityCollectionInfo(table.getModel().getRowCount(), table.getModel().getColumnCount(),
+ false, AccessibilityCollectionInfo.SELECTION_SINGLE);
+ }
+ if (component instanceof Tabs) {
+ return new AccessibilityCollectionInfo(1, ((Tabs)component).getTabCount(), false,
+ AccessibilityCollectionInfo.SELECTION_SINGLE);
+ }
+ return null;
+ }
+
+ private AccessibilityCollectionItemInfo inferCollectionItem(Component component) {
+ if (component.getParent() instanceof Table) {
+ Table table = (Table)component.getParent();
+ int sourceRow = table.getCellRow(component);
+ int row = sourceRow < 0 ? 0 : sourceRow + (table.isIncludeHeader() ? 1 : 0);
+ int column = table.getCellColumn(component);
+ return new AccessibilityCollectionItemInfo(row, 1, column, 1, column + 1,
+ table.getModel().getColumnCount(), 1, sourceRow < 0);
+ }
+ return null;
+ }
+
+ private Boolean inferReadOnly(Component component) {
+ return component instanceof TextArea ? Boolean.valueOf(!((TextArea)component).isEditable()) : null;
+ }
+
+ private Boolean inferMultiline(Component component) {
+ return component instanceof TextArea ? Boolean.valueOf(!(component instanceof TextField) || ((TextArea)component).getRows() > 1) : null;
+ }
+
+ private Boolean inferObscured(Component component) {
+ return component instanceof TextArea ? Boolean.valueOf((((TextArea)component).getConstraint() & TextArea.PASSWORD) != 0) : null;
+ }
+
+ private boolean shouldExpose(Component component, AccessibilityNode config, BuildNode node) {
+ if (component instanceof Form) return true;
+ if (config.hasExplicitConfiguration()) return true;
+ return node.builder.role != AccessibilityRole.NONE && (!isEmpty(node.builder.label)
+ || !isEmpty(node.builder.value) || node.builder.focusable || !node.builder.actions.isEmpty()
+ || node.builder.collectionInfo != null || node.builder.range != null);
+ }
+
+ private Rectangle componentBounds(Component component) {
+ Rectangle bounds = new Rectangle(component.getAbsoluteX() + component.getScrollX(),
+ component.getAbsoluteY() + component.getScrollY(), component.getWidth(), component.getHeight());
+ Container parent = component.getParent();
+ while (parent != null && bounds.getWidth() > 0 && bounds.getHeight() > 0) {
+ Rectangle clip = new Rectangle(parent.getAbsoluteX() + parent.getScrollX(),
+ parent.getAbsoluteY() + parent.getScrollY(), parent.getWidth(), parent.getHeight());
+ intersect(bounds, clip);
+ parent = parent.getParent();
+ }
+ return bounds;
+ }
+
+ private Rectangle virtualBounds(Component host, Rectangle relative) {
+ if (relative == null) return componentBounds(host);
+ Rectangle bounds = new Rectangle(host.getAbsoluteX() + host.getScrollX() + relative.getX(),
+ host.getAbsoluteY() + host.getScrollY() + relative.getY(), relative.getWidth(), relative.getHeight());
+ intersect(bounds, componentBounds(host));
+ return bounds;
+ }
+
+ private void intersect(Rectangle target, Rectangle clip) {
+ int x1 = Math.max(target.getX(), clip.getX());
+ int y1 = Math.max(target.getY(), clip.getY());
+ int x2 = Math.min(target.getX() + target.getWidth(), clip.getX() + clip.getWidth());
+ int y2 = Math.min(target.getY() + target.getHeight(), clip.getY() + clip.getHeight());
+ target.setBounds(x1, y1, Math.max(0, x2 - x1), Math.max(0, y2 - y1));
+ }
+
+ private void sortTree(List nodes) {
+ Collections.sort(nodes, new Comparator() {
+ public int compare(BuildNode a, BuildNode b) {
+ boolean an = Double.isNaN(a.builder.sortKey);
+ boolean bn = Double.isNaN(b.builder.sortKey);
+ if (an && bn) return 0;
+ if (an) return 1;
+ if (bn) return -1;
+ return a.builder.sortKey < b.builder.sortKey ? -1 : a.builder.sortKey == b.builder.sortKey ? 0 : 1;
+ }
+ });
+ applyRelativeOrder(nodes);
+ for (BuildNode node : nodes) sortTree(node.children);
+ }
+
+ private void applyRelativeOrder(List nodes) {
+ for (int pass = 0; pass < nodes.size(); pass++) {
+ boolean changed = false;
+ for (int i = 0; i < nodes.size(); i++) {
+ BuildNode node = nodes.get(i);
+ int target = indexOf(nodes, node.builder.traversalBefore);
+ if (target >= 0 && i > target) {
+ nodes.remove(i);
+ nodes.add(target, node);
+ changed = true;
+ break;
+ }
+ target = indexOf(nodes, node.builder.traversalAfter);
+ if (target >= 0 && i < target) {
+ nodes.remove(i);
+ nodes.add(Math.min(target, nodes.size()), node);
+ changed = true;
+ break;
+ }
+ }
+ if (!changed) return;
+ }
+ }
+
+ private int indexOf(List nodes, Component component) {
+ if (component == null) return -1;
+ for (int i = 0; i < nodes.size(); i++) if (nodes.get(i).component == component) return i;
+ return -1;
+ }
+
+ private void freeze(List source, long parentId, List childIds,
+ LinkedHashMap nodes) {
+ for (BuildNode node : source) {
+ node.builder.parentId = parentId;
+ for (BuildNode child : node.children) node.builder.childIds.add(Long.valueOf(child.id));
+ AccessibilityNodeSnapshot frozen = new AccessibilityNodeSnapshot(node.builder);
+ nodes.put(Long.valueOf(node.id), frozen);
+ childIds.add(Long.valueOf(node.id));
+ List ignored = new ArrayList();
+ freeze(node.children, node.id, ignored, nodes);
+ }
+ }
+
+ private String collectLabels(List nodes) {
+ String value = null;
+ for (BuildNode node : nodes) {
+ value = join(value, node.builder.label);
+ value = join(value, collectLabels(node.children));
+ }
+ return value;
+ }
+
+ private static boolean hasAction(List actions, String id) {
+ for (AccessibilityAction action : actions) if (id.equals(action.getId())) return true;
+ return false;
+ }
+
+ private static boolean isInteractiveRole(AccessibilityRole role) {
+ return role == AccessibilityRole.BUTTON || role == AccessibilityRole.TOGGLE_BUTTON
+ || role == AccessibilityRole.CHECKBOX || role == AccessibilityRole.RADIO_BUTTON
+ || role == AccessibilityRole.SWITCH || role == AccessibilityRole.LINK
+ || role == AccessibilityRole.TEXT_FIELD || role == AccessibilityRole.SEARCH_FIELD
+ || role == AccessibilityRole.SLIDER || role == AccessibilityRole.TAB
+ || role == AccessibilityRole.MENU_ITEM || role == AccessibilityRole.SPIN_BUTTON
+ || role == AccessibilityRole.COMBO_BOX || role == AccessibilityRole.TREE_ITEM;
+ }
+
+ private static String firstNonEmpty(String first, String second) {
+ return !isEmpty(first) ? first : second;
+ }
+
+ private static String join(String first, String second) {
+ if (isEmpty(first)) return second;
+ if (isEmpty(second)) return first;
+ return first + ", " + second;
+ }
+
+ private static boolean isEmpty(String value) {
+ return value == null || value.length() == 0;
+ }
+
+ private static final class BuildNode {
+ long id;
+ Component component;
+ AccessibilityNodeSnapshot.Builder builder = new AccessibilityNodeSnapshot.Builder();
+ List children = new ArrayList();
+ }
+}
diff --git a/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityNode.java b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityNode.java
new file mode 100644
index 00000000000..282fa251282
--- /dev/null
+++ b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityNode.java
@@ -0,0 +1,197 @@
+/*
+ * Copyright (c) 2026 Codename One and contributors. All rights reserved.
+ */
+package com.codename1.ui.accessibility;
+
+import com.codename1.ui.Component;
+import com.codename1.ui.geom.Rectangle;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/// Mutable semantic configuration for a lightweight component or virtual child.
+///
+/// Component-owned nodes are obtained with {@link Component#getSemantics()}.
+/// Applications can create standalone nodes for an {@link AccessibilityChildProvider};
+/// virtual nodes must have a stable {@link #setVirtualKey(java.lang.String) virtual key}.
+public class AccessibilityNode {
+ private Component owner;
+ private String virtualKey;
+ private String identifier;
+ private String label;
+ private String hint;
+ private String description;
+ private String value;
+ private String validationError;
+ private String paneTitle;
+ private String roleDescription;
+ private AccessibilityRole role = AccessibilityRole.NONE;
+ private AccessibilityCheckedState checked = AccessibilityCheckedState.UNSPECIFIED;
+ private AccessibilityLiveRegion liveRegion = AccessibilityLiveRegion.OFF;
+ private AccessibilityGrouping grouping = AccessibilityGrouping.AUTO;
+ private AccessibilityRange range;
+ private AccessibilityCollectionInfo collectionInfo;
+ private AccessibilityCollectionItemInfo collectionItemInfo;
+ private AccessibilityChildProvider childProvider;
+ private Rectangle bounds;
+ private Boolean selected;
+ private Boolean expanded;
+ private Boolean enabled;
+ private Boolean invalid;
+ private Boolean busy;
+ private Boolean readOnly;
+ private Boolean required;
+ private Boolean multiline;
+ private Boolean obscured;
+ private Boolean pressed;
+ private Boolean current;
+ private boolean modal;
+ private int headingLevel = -1;
+ private double sortKey = Double.NaN;
+ private Component traversalBefore;
+ private Component traversalAfter;
+ private final List actions = new ArrayList();
+ private final List children = new ArrayList();
+
+ public AccessibilityNode() {
+ }
+
+ public AccessibilityNode(String virtualKey) {
+ setVirtualKey(virtualKey);
+ }
+
+ /// Internal constructor used by {@link Component}.
+ public AccessibilityNode(Component owner) {
+ this.owner = owner;
+ }
+
+ private AccessibilityNode changed(int type) {
+ if (owner != null) {
+ AccessibilityManager.getInstance().invalidate(owner, type);
+ }
+ return this;
+ }
+
+ public Component getOwner() { return owner; }
+ public String getVirtualKey() { return virtualKey; }
+ public AccessibilityNode setVirtualKey(String virtualKey) {
+ if (virtualKey == null || virtualKey.length() == 0) {
+ throw new IllegalArgumentException("Virtual accessibility nodes require a non-empty stable key");
+ }
+ this.virtualKey = virtualKey;
+ return changed(AccessibilityManager.CHANGE_STRUCTURE);
+ }
+ public String getIdentifier() { return identifier; }
+ public AccessibilityNode setIdentifier(String identifier) { this.identifier = identifier; return changed(AccessibilityManager.CHANGE_CONTENT); }
+ public String getLabel() { return label; }
+ public AccessibilityNode setLabel(String label) { this.label = label; return changed(AccessibilityManager.CHANGE_CONTENT); }
+ public String getHint() { return hint; }
+ public AccessibilityNode setHint(String hint) { this.hint = hint; return changed(AccessibilityManager.CHANGE_CONTENT); }
+ public String getDescription() { return description; }
+ public AccessibilityNode setDescription(String description) { this.description = description; return changed(AccessibilityManager.CHANGE_CONTENT); }
+ public String getValue() { return value; }
+ public AccessibilityNode setValue(String value) { this.value = value; return changed(AccessibilityManager.CHANGE_VALUE); }
+ public String getValidationError() { return validationError; }
+ public AccessibilityNode setValidationError(String error) { this.validationError = error; this.invalid = error == null ? invalid : Boolean.TRUE; return changed(AccessibilityManager.CHANGE_STATE); }
+ public String getPaneTitle() { return paneTitle; }
+ public AccessibilityNode setPaneTitle(String paneTitle) { this.paneTitle = paneTitle; return changed(AccessibilityManager.CHANGE_PANE); }
+ public String getRoleDescription() { return roleDescription; }
+ public AccessibilityNode setRoleDescription(String roleDescription) { this.roleDescription = roleDescription; return changed(AccessibilityManager.CHANGE_CONTENT); }
+ public AccessibilityRole getRole() { return role; }
+ public AccessibilityNode setRole(AccessibilityRole role) { this.role = role == null ? AccessibilityRole.NONE : role; return changed(AccessibilityManager.CHANGE_STRUCTURE); }
+ public AccessibilityCheckedState getChecked() { return checked; }
+ public AccessibilityNode setChecked(AccessibilityCheckedState checked) { this.checked = checked == null ? AccessibilityCheckedState.UNSPECIFIED : checked; return changed(AccessibilityManager.CHANGE_STATE); }
+ public AccessibilityLiveRegion getLiveRegion() { return liveRegion; }
+ public AccessibilityNode setLiveRegion(AccessibilityLiveRegion liveRegion) { this.liveRegion = liveRegion == null ? AccessibilityLiveRegion.OFF : liveRegion; return changed(AccessibilityManager.CHANGE_LIVE_REGION); }
+ public AccessibilityGrouping getGrouping() { return grouping; }
+ public AccessibilityNode setGrouping(AccessibilityGrouping grouping) { this.grouping = grouping == null ? AccessibilityGrouping.AUTO : grouping; return changed(AccessibilityManager.CHANGE_STRUCTURE); }
+ public AccessibilityRange getRange() { return range; }
+ public AccessibilityNode setRange(AccessibilityRange range) { this.range = range; return changed(AccessibilityManager.CHANGE_VALUE); }
+ public AccessibilityCollectionInfo getCollectionInfo() { return collectionInfo; }
+ public AccessibilityNode setCollectionInfo(AccessibilityCollectionInfo info) { this.collectionInfo = info; return changed(AccessibilityManager.CHANGE_STRUCTURE); }
+ public AccessibilityCollectionItemInfo getCollectionItemInfo() { return collectionItemInfo; }
+ public AccessibilityNode setCollectionItemInfo(AccessibilityCollectionItemInfo info) { this.collectionItemInfo = info; return changed(AccessibilityManager.CHANGE_STRUCTURE); }
+ public AccessibilityChildProvider getChildProvider() { return childProvider; }
+ public AccessibilityNode setChildProvider(AccessibilityChildProvider provider) { this.childProvider = provider; return changed(AccessibilityManager.CHANGE_STRUCTURE); }
+ public Rectangle getBounds() { return bounds == null ? null : new Rectangle(bounds); }
+ public AccessibilityNode setBounds(Rectangle bounds) { this.bounds = bounds == null ? null : new Rectangle(bounds); return changed(AccessibilityManager.CHANGE_BOUNDS); }
+ public Boolean getSelected() { return selected; }
+ public AccessibilityNode setSelected(Boolean selected) { this.selected = selected; return changed(AccessibilityManager.CHANGE_STATE); }
+ public Boolean getExpanded() { return expanded; }
+ public AccessibilityNode setExpanded(Boolean expanded) { this.expanded = expanded; return changed(AccessibilityManager.CHANGE_STATE); }
+ public Boolean getEnabled() { return enabled; }
+ public AccessibilityNode setEnabled(Boolean enabled) { this.enabled = enabled; return changed(AccessibilityManager.CHANGE_STATE); }
+ public Boolean getInvalid() { return invalid; }
+ public AccessibilityNode setInvalid(Boolean invalid) { this.invalid = invalid; return changed(AccessibilityManager.CHANGE_STATE); }
+ public Boolean getBusy() { return busy; }
+ public AccessibilityNode setBusy(Boolean busy) { this.busy = busy; return changed(AccessibilityManager.CHANGE_STATE); }
+ public Boolean getReadOnly() { return readOnly; }
+ public AccessibilityNode setReadOnly(Boolean readOnly) { this.readOnly = readOnly; return changed(AccessibilityManager.CHANGE_STATE); }
+ public Boolean getRequired() { return required; }
+ public AccessibilityNode setRequired(Boolean required) { this.required = required; return changed(AccessibilityManager.CHANGE_STATE); }
+ public Boolean getMultiline() { return multiline; }
+ public AccessibilityNode setMultiline(Boolean multiline) { this.multiline = multiline; return changed(AccessibilityManager.CHANGE_STATE); }
+ public Boolean getObscured() { return obscured; }
+ public AccessibilityNode setObscured(Boolean obscured) { this.obscured = obscured; return changed(AccessibilityManager.CHANGE_STATE); }
+ public Boolean getPressed() { return pressed; }
+ public AccessibilityNode setPressed(Boolean pressed) { this.pressed = pressed; return changed(AccessibilityManager.CHANGE_STATE); }
+ public Boolean getCurrent() { return current; }
+ public AccessibilityNode setCurrent(Boolean current) { this.current = current; return changed(AccessibilityManager.CHANGE_STATE); }
+ public boolean isModal() { return modal; }
+ public AccessibilityNode setModal(boolean modal) { this.modal = modal; return changed(AccessibilityManager.CHANGE_PANE); }
+ public int getHeadingLevel() { return headingLevel; }
+ public AccessibilityNode setHeadingLevel(int level) { this.headingLevel = level; if (level > 0 && role == AccessibilityRole.NONE) role = AccessibilityRole.HEADING; return changed(AccessibilityManager.CHANGE_STRUCTURE); }
+ public double getSortKey() { return sortKey; }
+ public AccessibilityNode setSortKey(double sortKey) { this.sortKey = sortKey; return changed(AccessibilityManager.CHANGE_STRUCTURE); }
+ public Component getTraversalBefore() { return traversalBefore; }
+ public AccessibilityNode setTraversalBefore(Component component) { this.traversalBefore = component; return changed(AccessibilityManager.CHANGE_STRUCTURE); }
+ public Component getTraversalAfter() { return traversalAfter; }
+ public AccessibilityNode setTraversalAfter(Component component) { this.traversalAfter = component; return changed(AccessibilityManager.CHANGE_STRUCTURE); }
+
+ public AccessibilityNode addAction(AccessibilityAction action) {
+ if (action == null) throw new NullPointerException("action");
+ for (int i = actions.size() - 1; i >= 0; i--) {
+ if (actions.get(i).getId().equals(action.getId())) actions.remove(i);
+ }
+ actions.add(action);
+ return changed(AccessibilityManager.CHANGE_ACTIONS);
+ }
+
+ public AccessibilityNode removeAction(String id) {
+ for (int i = actions.size() - 1; i >= 0; i--) {
+ if (actions.get(i).getId().equals(id)) actions.remove(i);
+ }
+ return changed(AccessibilityManager.CHANGE_ACTIONS);
+ }
+
+ public List getActions() {
+ return Collections.unmodifiableList(actions);
+ }
+
+ public AccessibilityNode addChild(AccessibilityNode child) {
+ if (child == null) throw new NullPointerException("child");
+ children.add(child);
+ return changed(AccessibilityManager.CHANGE_STRUCTURE);
+ }
+
+ public AccessibilityNode clearChildren() {
+ children.clear();
+ return changed(AccessibilityManager.CHANGE_STRUCTURE);
+ }
+
+ public List getChildren() {
+ return Collections.unmodifiableList(children);
+ }
+
+ public boolean hasExplicitConfiguration() {
+ return role != AccessibilityRole.NONE || label != null || hint != null || description != null
+ || value != null || validationError != null || paneTitle != null || roleDescription != null
+ || checked != AccessibilityCheckedState.UNSPECIFIED || liveRegion != AccessibilityLiveRegion.OFF
+ || grouping != AccessibilityGrouping.AUTO || range != null || collectionInfo != null
+ || collectionItemInfo != null || childProvider != null || bounds != null || selected != null
+ || expanded != null || enabled != null || invalid != null || busy != null || readOnly != null
+ || required != null || multiline != null || obscured != null || pressed != null || current != null
+ || modal || headingLevel > 0 || !Double.isNaN(sortKey) || traversalBefore != null
+ || traversalAfter != null || !actions.isEmpty() || !children.isEmpty();
+ }
+}
diff --git a/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityNodeSnapshot.java b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityNodeSnapshot.java
new file mode 100644
index 00000000000..a60f35a51e4
--- /dev/null
+++ b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityNodeSnapshot.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright (c) 2026 Codename One and contributors. All rights reserved.
+ */
+package com.codename1.ui.accessibility;
+
+import com.codename1.ui.Component;
+import com.codename1.ui.geom.Rectangle;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/// Immutable resolved node exported to platform accessibility adapters and test tooling.
+public final class AccessibilityNodeSnapshot {
+ private final long id;
+ private final long parentId;
+ private final Component component;
+ private final String virtualKey;
+ private final String identifier;
+ private final String label;
+ private final String hint;
+ private final String description;
+ private final String value;
+ private final String validationError;
+ private final String paneTitle;
+ private final String roleDescription;
+ private final AccessibilityRole role;
+ private final AccessibilityCheckedState checked;
+ private final AccessibilityLiveRegion liveRegion;
+ private final AccessibilityRange range;
+ private final AccessibilityCollectionInfo collectionInfo;
+ private final AccessibilityCollectionItemInfo collectionItemInfo;
+ private final Rectangle bounds;
+ private final Boolean selected;
+ private final Boolean expanded;
+ private final Boolean enabled;
+ private final Boolean invalid;
+ private final Boolean busy;
+ private final Boolean readOnly;
+ private final Boolean required;
+ private final Boolean multiline;
+ private final Boolean obscured;
+ private final Boolean pressed;
+ private final Boolean current;
+ private final boolean modal;
+ private final boolean focusable;
+ private final boolean focused;
+ private final int headingLevel;
+ private final List childIds;
+ private final List actions;
+
+ AccessibilityNodeSnapshot(Builder b) {
+ id = b.id;
+ parentId = b.parentId;
+ component = b.component;
+ virtualKey = b.virtualKey;
+ identifier = b.identifier;
+ label = b.label;
+ hint = b.hint;
+ description = b.description;
+ value = b.value;
+ validationError = b.validationError;
+ paneTitle = b.paneTitle;
+ roleDescription = b.roleDescription;
+ role = b.role;
+ checked = b.checked;
+ liveRegion = b.liveRegion;
+ range = b.range;
+ collectionInfo = b.collectionInfo;
+ collectionItemInfo = b.collectionItemInfo;
+ bounds = b.bounds == null ? new Rectangle() : new Rectangle(b.bounds);
+ selected = b.selected;
+ expanded = b.expanded;
+ enabled = b.enabled;
+ invalid = b.invalid;
+ busy = b.busy;
+ readOnly = b.readOnly;
+ required = b.required;
+ multiline = b.multiline;
+ obscured = b.obscured;
+ pressed = b.pressed;
+ current = b.current;
+ modal = b.modal;
+ focusable = b.focusable;
+ focused = b.focused;
+ headingLevel = b.headingLevel;
+ childIds = Collections.unmodifiableList(new ArrayList(b.childIds));
+ actions = Collections.unmodifiableList(new ArrayList(b.actions));
+ }
+
+ public long getId() { return id; }
+ public long getParentId() { return parentId; }
+ public Component getComponent() { return component; }
+ public String getVirtualKey() { return virtualKey; }
+ public String getIdentifier() { return identifier; }
+ public String getLabel() { return label; }
+ public String getHint() { return hint; }
+ public String getDescription() { return description; }
+ public String getValue() { return value; }
+ public String getValidationError() { return validationError; }
+ public String getPaneTitle() { return paneTitle; }
+ public String getRoleDescription() { return roleDescription; }
+ public AccessibilityRole getRole() { return role; }
+ public AccessibilityCheckedState getChecked() { return checked; }
+ public AccessibilityLiveRegion getLiveRegion() { return liveRegion; }
+ public AccessibilityRange getRange() { return range; }
+ public AccessibilityCollectionInfo getCollectionInfo() { return collectionInfo; }
+ public AccessibilityCollectionItemInfo getCollectionItemInfo() { return collectionItemInfo; }
+ public Rectangle getBounds() { return new Rectangle(bounds); }
+ public Boolean getSelected() { return selected; }
+ public Boolean getExpanded() { return expanded; }
+ public Boolean getEnabled() { return enabled; }
+ public Boolean getInvalid() { return invalid; }
+ public Boolean getBusy() { return busy; }
+ public Boolean getReadOnly() { return readOnly; }
+ public Boolean getRequired() { return required; }
+ public Boolean getMultiline() { return multiline; }
+ public Boolean getObscured() { return obscured; }
+ public Boolean getPressed() { return pressed; }
+ public Boolean getCurrent() { return current; }
+ public boolean isModal() { return modal; }
+ public boolean isFocusable() { return focusable; }
+ public boolean isFocused() { return focused; }
+ public int getHeadingLevel() { return headingLevel; }
+ public List getChildIds() { return childIds; }
+ public List getActions() { return actions; }
+
+ public AccessibilityAction getAction(String id) {
+ if (id == null) return null;
+ for (AccessibilityAction action : actions) {
+ if (id.equals(action.getId())) return action;
+ }
+ return null;
+ }
+
+ static final class Builder {
+ long id;
+ long parentId = -1;
+ Component component;
+ String virtualKey;
+ String identifier;
+ String label;
+ String hint;
+ String description;
+ String value;
+ String validationError;
+ String paneTitle;
+ String roleDescription;
+ AccessibilityRole role = AccessibilityRole.GENERIC;
+ AccessibilityCheckedState checked = AccessibilityCheckedState.UNSPECIFIED;
+ AccessibilityLiveRegion liveRegion = AccessibilityLiveRegion.OFF;
+ AccessibilityRange range;
+ AccessibilityCollectionInfo collectionInfo;
+ AccessibilityCollectionItemInfo collectionItemInfo;
+ Rectangle bounds;
+ Boolean selected;
+ Boolean expanded;
+ Boolean enabled;
+ Boolean invalid;
+ Boolean busy;
+ Boolean readOnly;
+ Boolean required;
+ Boolean multiline;
+ Boolean obscured;
+ Boolean pressed;
+ Boolean current;
+ boolean modal;
+ boolean focusable;
+ boolean focused;
+ int headingLevel = -1;
+ double sortKey = Double.NaN;
+ Component traversalBefore;
+ Component traversalAfter;
+ List childIds = new ArrayList();
+ List actions = new ArrayList();
+ }
+}
diff --git a/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityRange.java b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityRange.java
new file mode 100644
index 00000000000..5a6abbe34fb
--- /dev/null
+++ b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityRange.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (c) 2026 Codename One and contributors. All rights reserved.
+ */
+package com.codename1.ui.accessibility;
+
+/// Numeric value metadata for sliders, progress indicators, spin buttons, and
+/// other adjustable controls.
+public final class AccessibilityRange {
+ private final double minimum;
+ private final double maximum;
+ private final double current;
+ private final double step;
+ private final String text;
+
+ public AccessibilityRange(double minimum, double maximum, double current) {
+ this(minimum, maximum, current, 0, null);
+ }
+
+ public AccessibilityRange(double minimum, double maximum, double current, double step) {
+ this(minimum, maximum, current, step, null);
+ }
+
+ public AccessibilityRange(double minimum, double maximum, double current, double step, String text) {
+ this.minimum = minimum;
+ this.maximum = maximum;
+ this.current = current;
+ this.step = step;
+ this.text = text;
+ }
+
+ public double getMinimum() {
+ return minimum;
+ }
+
+ public double getMaximum() {
+ return maximum;
+ }
+
+ public double getCurrent() {
+ return current;
+ }
+
+ public double getStep() {
+ return step;
+ }
+
+ public String getText() {
+ return text;
+ }
+
+ public boolean isValid() {
+ return !Double.isNaN(minimum) && !Double.isNaN(maximum) && !Double.isNaN(current)
+ && minimum <= maximum && current >= minimum && current <= maximum && step >= 0;
+ }
+}
diff --git a/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityRole.java b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityRole.java
new file mode 100644
index 00000000000..7dffde79736
--- /dev/null
+++ b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityRole.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (c) 2026 Codename One and contributors. All rights reserved.
+ */
+package com.codename1.ui.accessibility;
+
+/// The semantic role of a lightweight component or virtual accessibility node.
+///
+/// Roles are intentionally portable. Platform ports map them to native control
+/// types, traits, patterns, or ARIA roles and fall back to {@link #GENERIC} when
+/// a platform has no exact equivalent.
+public enum AccessibilityRole {
+ NONE,
+ GENERIC,
+ BUTTON,
+ TOGGLE_BUTTON,
+ CHECKBOX,
+ RADIO_BUTTON,
+ SWITCH,
+ HEADING,
+ LINK,
+ IMAGE,
+ STATIC_TEXT,
+ TEXT_FIELD,
+ SEARCH_FIELD,
+ SLIDER,
+ PROGRESS_BAR,
+ LIST,
+ LIST_ITEM,
+ GRID,
+ ROW,
+ CELL,
+ COLUMN_HEADER,
+ ROW_HEADER,
+ TAB_LIST,
+ TAB,
+ TAB_PANEL,
+ DIALOG,
+ ALERT,
+ MENU,
+ MENU_ITEM,
+ TOOLBAR,
+ SCROLL_BAR,
+ SPIN_BUTTON,
+ COMBO_BOX,
+ TREE,
+ TREE_ITEM,
+ SEPARATOR
+}
diff --git a/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityTreeSnapshot.java b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityTreeSnapshot.java
new file mode 100644
index 00000000000..a252a59cf16
--- /dev/null
+++ b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityTreeSnapshot.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright (c) 2026 Codename One and contributors. All rights reserved.
+ */
+package com.codename1.ui.accessibility;
+
+import com.codename1.ui.geom.Rectangle;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/// Immutable accessibility tree for the current form.
+public final class AccessibilityTreeSnapshot {
+ private final long generation;
+ private final List rootIds;
+ private final Map nodes;
+
+ AccessibilityTreeSnapshot(long generation, List rootIds, Map nodes) {
+ this.generation = generation;
+ this.rootIds = Collections.unmodifiableList(new ArrayList(rootIds));
+ this.nodes = Collections.unmodifiableMap(new LinkedHashMap(nodes));
+ }
+
+ public long getGeneration() { return generation; }
+ public List getRootIds() { return rootIds; }
+ public Map getNodes() { return nodes; }
+ public AccessibilityNodeSnapshot getNode(long id) { return nodes.get(Long.valueOf(id)); }
+
+ /// Returns the deepest semantic node containing the screen coordinate.
+ public AccessibilityNodeSnapshot getNodeAt(int x, int y) {
+ AccessibilityNodeSnapshot best = null;
+ for (AccessibilityNodeSnapshot node : nodes.values()) {
+ Rectangle r = node.getBounds();
+ if (r.getWidth() > 0 && r.getHeight() > 0 && r.contains(x, y)) {
+ if (best == null || isDescendant(node, best)) best = node;
+ }
+ }
+ return best;
+ }
+
+ private boolean isDescendant(AccessibilityNodeSnapshot possibleChild, AccessibilityNodeSnapshot possibleParent) {
+ long parent = possibleChild.getParentId();
+ while (parent >= 0) {
+ if (parent == possibleParent.getId()) return true;
+ AccessibilityNodeSnapshot node = getNode(parent);
+ if (node == null) return false;
+ parent = node.getParentId();
+ }
+ return false;
+ }
+
+ public String toJson() {
+ StringBuilder out = new StringBuilder();
+ out.append("{\"generation\":").append(generation).append(",\"roots\":[");
+ for (int i = 0; i < rootIds.size(); i++) {
+ if (i > 0) out.append(',');
+ out.append(rootIds.get(i));
+ }
+ out.append("],\"nodes\":[");
+ int index = 0;
+ for (AccessibilityNodeSnapshot node : nodes.values()) {
+ if (index++ > 0) out.append(',');
+ out.append('{');
+ out.append("\"id\":").append(node.getId());
+ out.append(",\"parent\":").append(node.getParentId());
+ out.append(",\"role\":"); appendJson(out, node.getRole().name());
+ out.append(",\"label\":"); appendJson(out, node.getLabel());
+ out.append(",\"value\":"); appendJson(out, node.getValue());
+ out.append(",\"hint\":"); appendJson(out, node.getHint());
+ out.append(",\"description\":"); appendJson(out, node.getDescription());
+ out.append(",\"error\":"); appendJson(out, node.getValidationError());
+ out.append(",\"paneTitle\":"); appendJson(out, node.getPaneTitle());
+ out.append(",\"identifier\":"); appendJson(out, node.getIdentifier());
+ out.append(",\"roleDescription\":"); appendJson(out, node.getRoleDescription());
+ out.append(",\"checked\":"); appendJson(out, node.getChecked().name());
+ out.append(",\"liveRegion\":"); appendJson(out, node.getLiveRegion().name());
+ out.append(",\"selected\":"); appendBoolean(out, node.getSelected());
+ out.append(",\"expanded\":"); appendBoolean(out, node.getExpanded());
+ out.append(",\"enabled\":"); appendBoolean(out, node.getEnabled());
+ out.append(",\"invalid\":"); appendBoolean(out, node.getInvalid());
+ out.append(",\"busy\":"); appendBoolean(out, node.getBusy());
+ out.append(",\"readOnly\":"); appendBoolean(out, node.getReadOnly());
+ out.append(",\"required\":"); appendBoolean(out, node.getRequired());
+ out.append(",\"multiline\":"); appendBoolean(out, node.getMultiline());
+ out.append(",\"obscured\":"); appendBoolean(out, node.getObscured());
+ out.append(",\"pressed\":"); appendBoolean(out, node.getPressed());
+ out.append(",\"current\":"); appendBoolean(out, node.getCurrent());
+ out.append(",\"modal\":").append(node.isModal());
+ out.append(",\"focusable\":").append(node.isFocusable());
+ out.append(",\"focused\":").append(node.isFocused());
+ out.append(",\"headingLevel\":").append(node.getHeadingLevel());
+ if (node.getRange() != null) {
+ AccessibilityRange range = node.getRange();
+ out.append(",\"range\":{");
+ out.append("\"min\":").append(range.getMinimum());
+ out.append(",\"max\":").append(range.getMaximum());
+ out.append(",\"current\":").append(range.getCurrent());
+ out.append(",\"step\":").append(range.getStep());
+ out.append(",\"text\":"); appendJson(out, range.getText());
+ out.append('}');
+ }
+ if (node.getCollectionInfo() != null) {
+ AccessibilityCollectionInfo collection = node.getCollectionInfo();
+ out.append(",\"collection\":{");
+ out.append("\"rows\":").append(collection.getRowCount());
+ out.append(",\"columns\":").append(collection.getColumnCount());
+ out.append(",\"hierarchical\":").append(collection.isHierarchical());
+ out.append(",\"selectionMode\":").append(collection.getSelectionMode()).append('}');
+ }
+ if (node.getCollectionItemInfo() != null) {
+ AccessibilityCollectionItemInfo item = node.getCollectionItemInfo();
+ out.append(",\"collectionItem\":{");
+ out.append("\"row\":").append(item.getRowIndex());
+ out.append(",\"rowSpan\":").append(item.getRowSpan());
+ out.append(",\"column\":").append(item.getColumnIndex());
+ out.append(",\"columnSpan\":").append(item.getColumnSpan());
+ out.append(",\"position\":").append(item.getPositionInSet());
+ out.append(",\"setSize\":").append(item.getSetSize());
+ out.append(",\"level\":").append(item.getLevel());
+ out.append(",\"heading\":").append(item.isHeading()).append('}');
+ }
+ Rectangle bounds = node.getBounds();
+ out.append(",\"bounds\":[").append(bounds.getX()).append(',').append(bounds.getY())
+ .append(',').append(bounds.getWidth()).append(',').append(bounds.getHeight()).append(']');
+ out.append(",\"children\":[");
+ for (int i = 0; i < node.getChildIds().size(); i++) {
+ if (i > 0) out.append(',');
+ out.append(node.getChildIds().get(i));
+ }
+ out.append("],\"actions\":[");
+ for (int i = 0; i < node.getActions().size(); i++) {
+ if (i > 0) out.append(',');
+ AccessibilityAction action = node.getActions().get(i);
+ out.append('{').append("\"id\":"); appendJson(out, action.getId());
+ out.append(",\"label\":"); appendJson(out, action.getLabel());
+ out.append(",\"enabled\":").append(action.isEnabled()).append('}');
+ }
+ out.append("]}");
+ }
+ out.append("]}");
+ return out.toString();
+ }
+
+ private static void appendJson(StringBuilder out, String value) {
+ if (value == null) {
+ out.append("null");
+ return;
+ }
+ out.append('"');
+ for (int i = 0; i < value.length(); i++) {
+ char c = value.charAt(i);
+ if (c == '"' || c == '\\') out.append('\\');
+ if (c == '\n') out.append("\\n");
+ else if (c == '\r') out.append("\\r");
+ else if (c == '\t') out.append("\\t");
+ else out.append(c);
+ }
+ out.append('"');
+ }
+
+ private static void appendBoolean(StringBuilder out, Boolean value) {
+ if (value == null) out.append("null");
+ else out.append(value.booleanValue());
+ }
+}
diff --git a/Ports/Android/src/com/codename1/impl/android/AndroidAccessibilityProvider.java b/Ports/Android/src/com/codename1/impl/android/AndroidAccessibilityProvider.java
new file mode 100644
index 00000000000..045eb526df1
--- /dev/null
+++ b/Ports/Android/src/com/codename1/impl/android/AndroidAccessibilityProvider.java
@@ -0,0 +1,361 @@
+/*
+ * Copyright (c) 2026 Codename One and contributors. All rights reserved.
+ */
+package com.codename1.impl.android;
+
+import android.graphics.Rect;
+import android.os.Build;
+import android.os.Bundle;
+import android.view.View;
+import android.view.ViewParent;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.accessibility.AccessibilityNodeProvider;
+import com.codename1.ui.accessibility.AccessibilityAction;
+import com.codename1.ui.accessibility.AccessibilityCheckedState;
+import com.codename1.ui.accessibility.AccessibilityCollectionInfo;
+import com.codename1.ui.accessibility.AccessibilityCollectionItemInfo;
+import com.codename1.ui.accessibility.AccessibilityLiveRegion;
+import com.codename1.ui.accessibility.AccessibilityManager;
+import com.codename1.ui.accessibility.AccessibilityNodeSnapshot;
+import com.codename1.ui.accessibility.AccessibilityRange;
+import com.codename1.ui.accessibility.AccessibilityRole;
+import com.codename1.ui.accessibility.AccessibilityTreeSnapshot;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/** Android virtual-view adapter for Codename One's lightweight semantic tree. */
+final class AndroidAccessibilityProvider extends AccessibilityNodeProvider {
+ private static final int CUSTOM_ACTION_BASE = 0x01000000;
+ private static final String ROLE_DESCRIPTION_KEY = "AccessibilityNodeInfo.roleDescription";
+ private final View host;
+ private final AndroidImplementation implementation;
+ private int accessibilityFocusedId = Integer.MIN_VALUE;
+
+ AndroidAccessibilityProvider(View host, AndroidImplementation implementation) {
+ this.host = host;
+ this.implementation = implementation;
+ host.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
+ }
+
+ @Override
+ public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) {
+ AccessibilityTreeSnapshot tree = implementation.getAccessibilityTreeSnapshot();
+ if (virtualViewId == View.NO_ID || virtualViewId == AccessibilityNodeProvider.HOST_VIEW_ID) {
+ return createHostNode(tree);
+ }
+ AccessibilityNodeSnapshot node = tree.getNode(virtualViewId);
+ return node == null ? null : createVirtualNode(tree, node, virtualViewId);
+ }
+
+ private AccessibilityNodeInfo createHostNode(AccessibilityTreeSnapshot tree) {
+ AccessibilityNodeInfo info = AccessibilityNodeInfo.obtain(host);
+ host.onInitializeAccessibilityNodeInfo(info);
+ info.setClassName("com.codename1.ui.Form");
+ info.setPackageName(host.getContext().getPackageName());
+ info.setScrollable(false);
+ for (Long rootId : tree.getRootIds()) {
+ info.addChild(host, toVirtualId(rootId.longValue()));
+ }
+ return info;
+ }
+
+ private AccessibilityNodeInfo createVirtualNode(AccessibilityTreeSnapshot tree,
+ AccessibilityNodeSnapshot node, int virtualViewId) {
+ AccessibilityNodeInfo info = AccessibilityNodeInfo.obtain();
+ info.setPackageName(host.getContext().getPackageName());
+ info.setClassName(className(node.getRole()));
+ info.setSource(host, virtualViewId);
+ if (node.getParentId() < 0) info.setParent(host);
+ else info.setParent(host, toVirtualId(node.getParentId()));
+ for (Long childId : node.getChildIds()) info.addChild(host, toVirtualId(childId.longValue()));
+
+ String label = node.getLabel();
+ if (usesText(node.getRole())) info.setText(label);
+ else info.setContentDescription(label);
+ if (Build.VERSION.SDK_INT >= 26 && node.getHint() != null) info.setHintText(node.getHint());
+ if (Build.VERSION.SDK_INT >= 19 && node.getRoleDescription() != null) {
+ info.getExtras().putCharSequence(ROLE_DESCRIPTION_KEY, node.getRoleDescription());
+ }
+ if (Build.VERSION.SDK_INT >= 18 && node.getIdentifier() != null) {
+ info.setViewIdResourceName(host.getContext().getPackageName() + ":id/" + node.getIdentifier());
+ }
+
+ boolean enabled = node.getEnabled() == null || node.getEnabled().booleanValue();
+ info.setEnabled(enabled);
+ info.setVisibleToUser(isVisible(node));
+ info.setFocusable(node.isFocusable());
+ info.setFocused(node.isFocused());
+ info.setAccessibilityFocused(virtualViewId == accessibilityFocusedId);
+ info.setSelected(Boolean.TRUE.equals(node.getSelected()));
+ info.setClickable(hasAction(node, AccessibilityAction.ACTIVATE));
+ info.setLongClickable(hasAction(node, AccessibilityAction.LONG_PRESS));
+ info.setScrollable(hasAction(node, AccessibilityAction.SCROLL_FORWARD)
+ || hasAction(node, AccessibilityAction.SCROLL_BACKWARD));
+
+ AccessibilityCheckedState checked = node.getChecked();
+ if (checked != AccessibilityCheckedState.UNSPECIFIED) {
+ info.setCheckable(true);
+ info.setChecked(checked == AccessibilityCheckedState.CHECKED || checked == AccessibilityCheckedState.MIXED);
+ if (checked == AccessibilityCheckedState.MIXED && Build.VERSION.SDK_INT >= 19) {
+ info.getExtras().putCharSequence("com.codename1.accessibility.checkedState", "mixed");
+ }
+ }
+ if (Build.VERSION.SDK_INT >= 19) {
+ info.setContentInvalid(Boolean.TRUE.equals(node.getInvalid()));
+ if (node.getCollectionInfo() != null) info.setCollectionInfo(collection(node.getCollectionInfo()));
+ if (node.getCollectionItemInfo() != null) info.setCollectionItemInfo(collectionItem(node.getCollectionItemInfo(), node));
+ if (node.getRange() != null) info.setRangeInfo(range(node.getRange()));
+ if (node.getExpanded() != null) {
+ info.getExtras().putBoolean("com.codename1.accessibility.expanded", node.getExpanded().booleanValue());
+ }
+ }
+ if (Build.VERSION.SDK_INT >= 21 && node.getValidationError() != null) info.setError(node.getValidationError());
+ if (Build.VERSION.SDK_INT >= 28) {
+ info.setHeading(node.getHeadingLevel() > 0
+ || node.getRole() == AccessibilityRole.HEADING
+ || node.getCollectionItemInfo() != null && node.getCollectionItemInfo().isHeading());
+ info.setScreenReaderFocusable(node.isFocusable() || label != null);
+ if (node.getPaneTitle() != null) info.setPaneTitle(node.getPaneTitle());
+ }
+ if (Build.VERSION.SDK_INT >= 19) {
+ info.setLiveRegion(node.getLiveRegion() == AccessibilityLiveRegion.ASSERTIVE
+ ? View.ACCESSIBILITY_LIVE_REGION_ASSERTIVE
+ : node.getLiveRegion() == AccessibilityLiveRegion.POLITE
+ ? View.ACCESSIBILITY_LIVE_REGION_POLITE : View.ACCESSIBILITY_LIVE_REGION_NONE);
+ }
+
+ addActions(info, node);
+ setBounds(info, node);
+ return info;
+ }
+
+ private void addActions(AccessibilityNodeInfo info, AccessibilityNodeSnapshot node) {
+ if (node.isFocusable()) info.addAction(AccessibilityNodeInfo.ACTION_FOCUS);
+ if (hasAction(node, AccessibilityAction.ACTIVATE)) info.addAction(AccessibilityNodeInfo.ACTION_CLICK);
+ if (hasAction(node, AccessibilityAction.LONG_PRESS)) info.addAction(AccessibilityNodeInfo.ACTION_LONG_CLICK);
+ if (hasAction(node, AccessibilityAction.INCREMENT)) info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
+ if (hasAction(node, AccessibilityAction.DECREMENT)) info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD);
+ if (hasAction(node, AccessibilityAction.SCROLL_FORWARD)) info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
+ if (hasAction(node, AccessibilityAction.SCROLL_BACKWARD)) info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD);
+ info.addAction(accessibilityFocusedId == toVirtualId(node.getId())
+ ? AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS
+ : AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS);
+ if (Build.VERSION.SDK_INT >= 21) {
+ for (AccessibilityAction action : node.getActions()) {
+ if (isStandard(action.getId())) continue;
+ info.addAction(new AccessibilityNodeInfo.AccessibilityAction(customActionId(action.getId()),
+ action.getLabel() == null ? action.getId() : action.getLabel()));
+ }
+ if (hasAction(node, AccessibilityAction.SET_TEXT)) {
+ info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SET_TEXT);
+ }
+ }
+ }
+
+ @Override
+ public boolean performAction(int virtualViewId, int action, Bundle arguments) {
+ if (virtualViewId == AccessibilityNodeProvider.HOST_VIEW_ID) {
+ return host.performAccessibilityAction(action, arguments);
+ }
+ AccessibilityNodeSnapshot node = implementation.getAccessibilityTreeSnapshot().getNode(virtualViewId);
+ if (node == null) return false;
+ if (action == AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS) {
+ if (accessibilityFocusedId == virtualViewId) return false;
+ int previous = accessibilityFocusedId;
+ accessibilityFocusedId = virtualViewId;
+ if (previous != Integer.MIN_VALUE) sendEvent(previous, AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
+ sendEvent(virtualViewId, AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
+ return true;
+ }
+ if (action == AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS) {
+ if (accessibilityFocusedId != virtualViewId) return false;
+ accessibilityFocusedId = Integer.MIN_VALUE;
+ sendEvent(virtualViewId, AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
+ return true;
+ }
+ String actionId = null;
+ Object argument = null;
+ if (action == AccessibilityNodeInfo.ACTION_CLICK) actionId = AccessibilityAction.ACTIVATE;
+ else if (action == AccessibilityNodeInfo.ACTION_LONG_CLICK) actionId = AccessibilityAction.LONG_PRESS;
+ else if (action == AccessibilityNodeInfo.ACTION_FOCUS) actionId = AccessibilityAction.FOCUS;
+ else if (action == AccessibilityNodeInfo.ACTION_SCROLL_FORWARD) {
+ actionId = hasAction(node, AccessibilityAction.INCREMENT)
+ ? AccessibilityAction.INCREMENT : AccessibilityAction.SCROLL_FORWARD;
+ } else if (action == AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD) {
+ actionId = hasAction(node, AccessibilityAction.DECREMENT)
+ ? AccessibilityAction.DECREMENT : AccessibilityAction.SCROLL_BACKWARD;
+ } else if (Build.VERSION.SDK_INT >= 21 && action == AccessibilityNodeInfo.ACTION_SET_TEXT) {
+ actionId = AccessibilityAction.SET_TEXT;
+ argument = arguments == null ? null : arguments.getCharSequence(
+ AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE);
+ if (argument != null) argument = argument.toString();
+ } else {
+ for (AccessibilityAction candidate : node.getActions()) {
+ if (customActionId(candidate.getId()) == action) {
+ actionId = candidate.getId();
+ break;
+ }
+ }
+ }
+ return actionId != null && implementation.performAccessibilityAction(node.getId(), actionId, argument);
+ }
+
+ @Override
+ public List findAccessibilityNodeInfosByText(String searched, int virtualViewId) {
+ if (searched == null) return Collections.emptyList();
+ String lower = searched.toLowerCase();
+ List result = new ArrayList();
+ AccessibilityTreeSnapshot tree = implementation.getAccessibilityTreeSnapshot();
+ for (AccessibilityNodeSnapshot node : tree.getNodes().values()) {
+ if (contains(node.getLabel(), lower) || contains(node.getValue(), lower)
+ || contains(node.getDescription(), lower)) {
+ result.add(createVirtualNode(tree, node, toVirtualId(node.getId())));
+ }
+ }
+ return result;
+ }
+
+ @Override
+ public AccessibilityNodeInfo findFocus(int focus) {
+ AccessibilityTreeSnapshot tree = implementation.getAccessibilityTreeSnapshot();
+ if (focus == AccessibilityNodeInfo.FOCUS_ACCESSIBILITY && accessibilityFocusedId != Integer.MIN_VALUE) {
+ AccessibilityNodeSnapshot node = tree.getNode(accessibilityFocusedId);
+ return node == null ? null : createVirtualNode(tree, node, accessibilityFocusedId);
+ }
+ if (focus == AccessibilityNodeInfo.FOCUS_INPUT) {
+ for (AccessibilityNodeSnapshot node : tree.getNodes().values()) {
+ if (node.isFocused()) return createVirtualNode(tree, node, toVirtualId(node.getId()));
+ }
+ }
+ return null;
+ }
+
+ void invalidate(int changes) {
+ if ((changes & AccessibilityManager.CHANGE_STRUCTURE) != 0
+ || (changes & AccessibilityManager.CHANGE_PANE) != 0) {
+ sendHostEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
+ } else if ((changes & AccessibilityManager.CHANGE_FOCUS) != 0) {
+ sendHostEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED);
+ } else {
+ sendHostEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
+ }
+ }
+
+ private void sendHostEvent(int type) {
+ AccessibilityEvent event = AccessibilityEvent.obtain(type);
+ event.setPackageName(host.getContext().getPackageName());
+ event.setClassName("com.codename1.ui.Form");
+ event.setSource(host);
+ send(event);
+ }
+
+ private void sendEvent(int virtualId, int type) {
+ AccessibilityEvent event = AccessibilityEvent.obtain(type);
+ event.setPackageName(host.getContext().getPackageName());
+ event.setClassName("com.codename1.ui.Component");
+ event.setSource(host, virtualId);
+ send(event);
+ }
+
+ private void send(AccessibilityEvent event) {
+ ViewParent parent = host.getParent();
+ if (parent != null) parent.requestSendAccessibilityEvent(host, event);
+ }
+
+ private void setBounds(AccessibilityNodeInfo info, AccessibilityNodeSnapshot node) {
+ com.codename1.ui.geom.Rectangle b = node.getBounds();
+ Rect parent = new Rect(b.getX(), b.getY(), b.getX() + b.getWidth(), b.getY() + b.getHeight());
+ info.setBoundsInParent(parent);
+ int[] location = new int[2];
+ host.getLocationOnScreen(location);
+ Rect screen = new Rect(parent);
+ screen.offset(location[0], location[1]);
+ info.setBoundsInScreen(screen);
+ }
+
+ private boolean isVisible(AccessibilityNodeSnapshot node) {
+ com.codename1.ui.geom.Rectangle b = node.getBounds();
+ return host.isShown() && b.getWidth() > 0 && b.getHeight() > 0;
+ }
+
+ private AccessibilityNodeInfo.CollectionInfo collection(AccessibilityCollectionInfo info) {
+ int mode = info.getSelectionMode() == AccessibilityCollectionInfo.SELECTION_MULTIPLE
+ ? AccessibilityNodeInfo.CollectionInfo.SELECTION_MODE_MULTIPLE
+ : info.getSelectionMode() == AccessibilityCollectionInfo.SELECTION_SINGLE
+ ? AccessibilityNodeInfo.CollectionInfo.SELECTION_MODE_SINGLE
+ : AccessibilityNodeInfo.CollectionInfo.SELECTION_MODE_NONE;
+ if (Build.VERSION.SDK_INT >= 21) {
+ return AccessibilityNodeInfo.CollectionInfo.obtain(info.getRowCount(), info.getColumnCount(),
+ info.isHierarchical(), mode);
+ }
+ return AccessibilityNodeInfo.CollectionInfo.obtain(info.getRowCount(), info.getColumnCount(),
+ info.isHierarchical());
+ }
+
+ private AccessibilityNodeInfo.CollectionItemInfo collectionItem(AccessibilityCollectionItemInfo item,
+ AccessibilityNodeSnapshot node) {
+ if (Build.VERSION.SDK_INT >= 21) {
+ return AccessibilityNodeInfo.CollectionItemInfo.obtain(item.getRowIndex(), Math.max(1, item.getRowSpan()),
+ item.getColumnIndex(), Math.max(1, item.getColumnSpan()), item.isHeading(),
+ Boolean.TRUE.equals(node.getSelected()));
+ }
+ return AccessibilityNodeInfo.CollectionItemInfo.obtain(item.getRowIndex(), Math.max(1, item.getRowSpan()),
+ item.getColumnIndex(), Math.max(1, item.getColumnSpan()), item.isHeading());
+ }
+
+ private AccessibilityNodeInfo.RangeInfo range(AccessibilityRange range) {
+ return AccessibilityNodeInfo.RangeInfo.obtain(AccessibilityNodeInfo.RangeInfo.RANGE_TYPE_FLOAT,
+ (float)range.getMinimum(), (float)range.getMaximum(), (float)range.getCurrent());
+ }
+
+ private String className(AccessibilityRole role) {
+ switch (role) {
+ case BUTTON: return "android.widget.Button";
+ case TOGGLE_BUTTON: return "android.widget.ToggleButton";
+ case CHECKBOX: return "android.widget.CheckBox";
+ case RADIO_BUTTON: return "android.widget.RadioButton";
+ case SWITCH: return "android.widget.Switch";
+ case TEXT_FIELD:
+ case SEARCH_FIELD: return "android.widget.EditText";
+ case SLIDER: return "android.widget.SeekBar";
+ case PROGRESS_BAR: return "android.widget.ProgressBar";
+ case LIST: return "android.widget.ListView";
+ case GRID: return "android.widget.GridView";
+ case IMAGE: return "android.widget.ImageView";
+ case TAB: return "android.app.ActionBar$Tab";
+ case DIALOG:
+ case ALERT: return "android.app.Dialog";
+ default: return "android.view.View";
+ }
+ }
+
+ private boolean usesText(AccessibilityRole role) {
+ return role == AccessibilityRole.STATIC_TEXT || role == AccessibilityRole.TEXT_FIELD
+ || role == AccessibilityRole.SEARCH_FIELD || role == AccessibilityRole.HEADING;
+ }
+
+ private boolean contains(String value, String searchedLower) {
+ return value != null && value.toLowerCase().contains(searchedLower);
+ }
+
+ private boolean hasAction(AccessibilityNodeSnapshot node, String id) {
+ return node.getAction(id) != null;
+ }
+
+ private boolean isStandard(String id) {
+ return AccessibilityAction.ACTIVATE.equals(id) || AccessibilityAction.LONG_PRESS.equals(id)
+ || AccessibilityAction.INCREMENT.equals(id) || AccessibilityAction.DECREMENT.equals(id)
+ || AccessibilityAction.FOCUS.equals(id) || AccessibilityAction.SET_TEXT.equals(id)
+ || AccessibilityAction.SCROLL_FORWARD.equals(id) || AccessibilityAction.SCROLL_BACKWARD.equals(id);
+ }
+
+ private int customActionId(String id) {
+ return CUSTOM_ACTION_BASE | (id.hashCode() & 0x00ffffff);
+ }
+
+ private int toVirtualId(long id) {
+ return (int)id;
+ }
+}
diff --git a/Ports/Android/src/com/codename1/impl/android/AndroidImplementation.java b/Ports/Android/src/com/codename1/impl/android/AndroidImplementation.java
index 1b26a84c9cc..17feb3e03ab 100644
--- a/Ports/Android/src/com/codename1/impl/android/AndroidImplementation.java
+++ b/Ports/Android/src/com/codename1/impl/android/AndroidImplementation.java
@@ -317,7 +317,8 @@ public static void setActivity(CodenameOneActivity aActivity) {
}
}
- CodenameOneSurface myView = null;
+ CodenameOneSurface myView = null;
+ private AndroidAccessibilityProvider accessibilityProvider;
CodenameOneTextPaint defaultFont;
private final char[] tmpchar = new char[1];
private final Rect tmprect = new Rect();
@@ -1628,7 +1629,18 @@ private void initSurface() {
superPeerMode = true;
myView = new AndroidAsyncView(getActivity(), AndroidImplementation.this);
}
- myView.getAndroidView().setVisibility(View.VISIBLE);
+ myView.getAndroidView().setVisibility(View.VISIBLE);
+
+ if (Build.VERSION.SDK_INT >= 16) {
+ final View semanticHost = myView.getAndroidView();
+ accessibilityProvider = new AndroidAccessibilityProvider(semanticHost, this);
+ semanticHost.setAccessibilityDelegate(new View.AccessibilityDelegate() {
+ @Override
+ public android.view.accessibility.AccessibilityNodeProvider getAccessibilityNodeProvider(View host) {
+ return accessibilityProvider;
+ }
+ });
+ }
relativeLayout.addView(myView.getAndroidView());
myView.getAndroidView().setVisibility(View.VISIBLE);
@@ -12891,7 +12903,7 @@ public void run() {
}
@Override
- public void announceForAccessibility(final Component cmp, final String text) {
+ public void announceForAccessibility(final Component cmp, final String text) {
final Activity act = getActivity();
if (act == null) {
return;
@@ -12925,7 +12937,23 @@ public void run() {
}
}
});
- }
+ }
+
+ @Override
+ public void accessibilityTreeChanged(final int changeType) {
+ final Activity act = getActivity();
+ if (act == null || accessibilityProvider == null) return;
+ act.runOnUiThread(new Runnable() {
+ public void run() {
+ if (accessibilityProvider != null) accessibilityProvider.invalidate(changeType);
+ }
+ });
+ }
+
+ @Override
+ public boolean isAccessibilityTreeSupported() {
+ return Build.VERSION.SDK_INT >= 16;
+ }
// ================================================================
// Crypto bridge -- routes com.codename1.security onto the standard
diff --git a/Ports/JavaSE/src/com/codename1/impl/javase/ComponentTreeInspector.java b/Ports/JavaSE/src/com/codename1/impl/javase/ComponentTreeInspector.java
index e2463f15ceb..4cd8bbcfad2 100644
--- a/Ports/JavaSE/src/com/codename1/impl/javase/ComponentTreeInspector.java
+++ b/Ports/JavaSE/src/com/codename1/impl/javase/ComponentTreeInspector.java
@@ -22,8 +22,14 @@
import com.codename1.ui.list.ContainerList;
import com.codename1.ui.plaf.UIManager;
import com.codename1.ui.util.Resources;
+import com.codename1.ui.accessibility.AccessibilityAssertions;
+import com.codename1.ui.accessibility.AccessibilityInspector;
+import com.codename1.ui.accessibility.AccessibilityIssue;
+import com.codename1.ui.accessibility.AccessibilityTreeSnapshot;
import java.awt.BorderLayout;
import java.awt.Desktop;
+import java.awt.Toolkit;
+import java.awt.datatransfer.StringSelection;
import java.awt.event.ActionEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
@@ -179,6 +185,54 @@ public void run() {
});
contextMenu.add(printComponent);
+ JMenuItem copyAccessibilityTree = new JMenuItem("Copy Accessibility Tree JSON");
+ copyAccessibilityTree.addActionListener(new java.awt.event.ActionListener() {
+ public void actionPerformed(java.awt.event.ActionEvent evt) {
+ com.codename1.ui.CN.callSerially(new Runnable() {
+ public void run() {
+ Form form = Display.getInstance().getCurrent();
+ if (form == null) return;
+ final String json = AccessibilityInspector.snapshot(form).toJson();
+ SwingUtilities.invokeLater(new Runnable() {
+ public void run() {
+ Toolkit.getDefaultToolkit().getSystemClipboard().setContents(
+ new StringSelection(json), null);
+ }
+ });
+ }
+ });
+ }
+ });
+ contextMenu.add(copyAccessibilityTree);
+
+ JMenuItem auditAccessibilityTree = new JMenuItem("Audit Accessibility Tree");
+ auditAccessibilityTree.addActionListener(new java.awt.event.ActionListener() {
+ public void actionPerformed(java.awt.event.ActionEvent evt) {
+ com.codename1.ui.CN.callSerially(new Runnable() {
+ public void run() {
+ Form form = Display.getInstance().getCurrent();
+ if (form == null) return;
+ AccessibilityTreeSnapshot snapshot = AccessibilityInspector.snapshot(form);
+ final List issues = AccessibilityAssertions.audit(snapshot);
+ final StringBuilder message = new StringBuilder();
+ for (AccessibilityIssue issue : issues) {
+ if (message.length() > 0) message.append('\n');
+ message.append(issue.toString());
+ }
+ SwingUtilities.invokeLater(new Runnable() {
+ public void run() {
+ JOptionPane.showMessageDialog(ComponentTreeInspector.this,
+ issues.isEmpty() ? "No accessibility issues found" : message.toString(),
+ "Accessibility Audit", issues.isEmpty()
+ ? JOptionPane.INFORMATION_MESSAGE : JOptionPane.WARNING_MESSAGE);
+ }
+ });
+ }
+ });
+ }
+ });
+ contextMenu.add(auditAccessibilityTree);
+
//if (currentComponent instanceof Container) {
JMenuItem revalidate = new JMenuItem("Revalidate");
revalidate.addActionListener(new java.awt.event.ActionListener() {
diff --git a/Ports/JavaSE/src/com/codename1/impl/javase/JavaSEAccessibility.java b/Ports/JavaSE/src/com/codename1/impl/javase/JavaSEAccessibility.java
new file mode 100644
index 00000000000..14b60cf0e62
--- /dev/null
+++ b/Ports/JavaSE/src/com/codename1/impl/javase/JavaSEAccessibility.java
@@ -0,0 +1,290 @@
+/*
+ * Copyright (c) 2026 Codename One and contributors. All rights reserved.
+ */
+package com.codename1.impl.javase;
+
+import com.codename1.ui.accessibility.AccessibilityAction;
+import com.codename1.ui.accessibility.AccessibilityCheckedState;
+import com.codename1.ui.accessibility.AccessibilityNodeSnapshot;
+import com.codename1.ui.accessibility.AccessibilityRange;
+import com.codename1.ui.accessibility.AccessibilityRole;
+import com.codename1.ui.accessibility.AccessibilityTreeSnapshot;
+import java.awt.Color;
+import java.awt.Cursor;
+import java.awt.Dimension;
+import java.awt.Font;
+import java.awt.FontMetrics;
+import java.awt.Point;
+import java.awt.Rectangle;
+import java.awt.event.FocusListener;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import javax.accessibility.Accessible;
+import javax.accessibility.AccessibleAction;
+import javax.accessibility.AccessibleComponent;
+import javax.accessibility.AccessibleContext;
+import javax.accessibility.AccessibleRole;
+import javax.accessibility.AccessibleState;
+import javax.accessibility.AccessibleStateSet;
+import javax.accessibility.AccessibleValue;
+import javax.swing.JPanel;
+
+/** Swing/Java Access Bridge projection of the lightweight semantic tree. */
+final class JavaSEAccessibility {
+ private final JPanel canvas;
+ private final JavaSEPort implementation;
+ private final RootContext root = new RootContext();
+ private final Map cache = new HashMap();
+
+ JavaSEAccessibility(JPanel canvas, JavaSEPort implementation) {
+ this.canvas = canvas;
+ this.implementation = implementation;
+ }
+
+ AccessibleContext getContext() {
+ return root;
+ }
+
+ void changed(int changeType) {
+ root.firePropertyChange(AccessibleContext.ACCESSIBLE_VISIBLE_DATA_PROPERTY, null,
+ Integer.valueOf(changeType));
+ }
+
+ private AccessibilityTreeSnapshot tree() {
+ return implementation.getAccessibilityTreeSnapshot();
+ }
+
+ private VirtualAccessible accessible(long id) {
+ VirtualAccessible value = cache.get(Long.valueOf(id));
+ if (value == null) {
+ value = new VirtualAccessible(id);
+ cache.put(Long.valueOf(id), value);
+ }
+ return value;
+ }
+
+ private final class RootContext extends AccessibleContext implements AccessibleComponent {
+ RootContext() {
+ setAccessibleParent(canvas);
+ }
+
+ public String getAccessibleName() { return "Codename One application"; }
+ public String getAccessibleDescription() { return "Lightweight component accessibility tree"; }
+ public AccessibleRole getAccessibleRole() { return AccessibleRole.PANEL; }
+ public AccessibleStateSet getAccessibleStateSet() {
+ AccessibleStateSet states = new AccessibleStateSet();
+ if (canvas.isEnabled()) states.add(AccessibleState.ENABLED);
+ if (canvas.isVisible()) states.add(AccessibleState.VISIBLE);
+ if (canvas.isShowing()) states.add(AccessibleState.SHOWING);
+ states.add(AccessibleState.FOCUSABLE);
+ if (canvas.hasFocus()) states.add(AccessibleState.FOCUSED);
+ return states;
+ }
+ public int getAccessibleIndexInParent() { return -1; }
+ public int getAccessibleChildrenCount() { return tree().getRootIds().size(); }
+ public Accessible getAccessibleChild(int i) {
+ List roots = tree().getRootIds();
+ return i < 0 || i >= roots.size() ? null : accessible(roots.get(i).longValue());
+ }
+ public Locale getLocale() { return Locale.getDefault(); }
+ public AccessibleComponent getAccessibleComponent() { return this; }
+ public Color getBackground() { return canvas.getBackground(); }
+ public void setBackground(Color c) { canvas.setBackground(c); }
+ public Color getForeground() { return canvas.getForeground(); }
+ public void setForeground(Color c) { canvas.setForeground(c); }
+ public Cursor getCursor() { return canvas.getCursor(); }
+ public void setCursor(Cursor c) { canvas.setCursor(c); }
+ public Font getFont() { return canvas.getFont(); }
+ public void setFont(Font f) { canvas.setFont(f); }
+ public FontMetrics getFontMetrics(Font f) { return canvas.getFontMetrics(f); }
+ public boolean isEnabled() { return canvas.isEnabled(); }
+ public void setEnabled(boolean b) { canvas.setEnabled(b); }
+ public boolean isVisible() { return canvas.isVisible(); }
+ public void setVisible(boolean b) { canvas.setVisible(b); }
+ public boolean isShowing() { return canvas.isShowing(); }
+ public boolean contains(Point p) { return canvas.contains(p); }
+ public Point getLocationOnScreen() { return canvas.getLocationOnScreen(); }
+ public Point getLocation() { return canvas.getLocation(); }
+ public void setLocation(Point p) { canvas.setLocation(p); }
+ public Rectangle getBounds() { return canvas.getBounds(); }
+ public void setBounds(Rectangle r) { canvas.setBounds(r); }
+ public Dimension getSize() { return canvas.getSize(); }
+ public void setSize(Dimension d) { canvas.setSize(d); }
+ public Accessible getAccessibleAt(Point p) {
+ Point local = new Point(p);
+ AccessibilityNodeSnapshot node = tree().getNodeAt(local.x, local.y);
+ return node == null ? null : accessible(node.getId());
+ }
+ public boolean isFocusTraversable() { return true; }
+ public void requestFocus() { canvas.requestFocus(); }
+ public void addFocusListener(FocusListener l) { canvas.addFocusListener(l); }
+ public void removeFocusListener(FocusListener l) { canvas.removeFocusListener(l); }
+ }
+
+ private final class VirtualAccessible implements Accessible {
+ private final long id;
+ private final NodeContext context;
+
+ VirtualAccessible(long id) {
+ this.id = id;
+ context = new NodeContext();
+ }
+
+ public AccessibleContext getAccessibleContext() { return context; }
+
+ private AccessibilityNodeSnapshot node() { return tree().getNode(id); }
+
+ private final class NodeContext extends AccessibleContext
+ implements AccessibleComponent, AccessibleAction, AccessibleValue {
+ NodeContext() {
+ setAccessibleParent(canvas);
+ }
+
+ public String getAccessibleName() { return node() == null ? null : node().getLabel(); }
+ public void setAccessibleName(String name) { }
+ public String getAccessibleDescription() {
+ AccessibilityNodeSnapshot n = node();
+ if (n == null) return null;
+ String value = n.getDescription();
+ if (n.getHint() != null) value = value == null ? n.getHint() : value + ". " + n.getHint();
+ if (n.getValidationError() != null) value = value == null ? n.getValidationError() : value + ". " + n.getValidationError();
+ return value;
+ }
+ public void setAccessibleDescription(String description) { }
+ public AccessibleRole getAccessibleRole() { return role(node() == null ? AccessibilityRole.GENERIC : node().getRole()); }
+ public AccessibleStateSet getAccessibleStateSet() { return states(node()); }
+ public Accessible getAccessibleParent() {
+ AccessibilityNodeSnapshot n = node();
+ return n == null || n.getParentId() < 0 ? canvas : accessible(n.getParentId());
+ }
+ public int getAccessibleIndexInParent() {
+ AccessibilityNodeSnapshot n = node();
+ if (n == null) return -1;
+ List siblings = n.getParentId() < 0 ? tree().getRootIds()
+ : tree().getNode(n.getParentId()).getChildIds();
+ return siblings.indexOf(Long.valueOf(id));
+ }
+ public int getAccessibleChildrenCount() { return node() == null ? 0 : node().getChildIds().size(); }
+ public Accessible getAccessibleChild(int i) {
+ AccessibilityNodeSnapshot n = node();
+ if (n == null || i < 0 || i >= n.getChildIds().size()) return null;
+ return accessible(n.getChildIds().get(i).longValue());
+ }
+ public Locale getLocale() { return Locale.getDefault(); }
+ public AccessibleComponent getAccessibleComponent() { return this; }
+ public AccessibleAction getAccessibleAction() { return node() != null && !node().getActions().isEmpty() ? this : null; }
+ public AccessibleValue getAccessibleValue() { return node() != null && node().getRange() != null ? this : null; }
+ public int getAccessibleActionCount() { return node() == null ? 0 : node().getActions().size(); }
+ public String getAccessibleActionDescription(int i) {
+ if (node() == null || i < 0 || i >= node().getActions().size()) return null;
+ AccessibilityAction action = node().getActions().get(i);
+ return action.getLabel() == null ? action.getId() : action.getLabel();
+ }
+ public boolean doAccessibleAction(int i) {
+ if (node() == null || i < 0 || i >= node().getActions().size()) return false;
+ return implementation.performAccessibilityAction(id, node().getActions().get(i).getId(), null);
+ }
+ public Number getCurrentAccessibleValue() { return node() == null || node().getRange() == null ? null : Double.valueOf(node().getRange().getCurrent()); }
+ public boolean setCurrentAccessibleValue(Number value) {
+ return value != null && implementation.performAccessibilityAction(id, AccessibilityAction.SET_VALUE, value);
+ }
+ public Number getMinimumAccessibleValue() { return node() == null || node().getRange() == null ? null : Double.valueOf(node().getRange().getMinimum()); }
+ public Number getMaximumAccessibleValue() { return node() == null || node().getRange() == null ? null : Double.valueOf(node().getRange().getMaximum()); }
+ public Color getBackground() { return canvas.getBackground(); }
+ public void setBackground(Color c) { }
+ public Color getForeground() { return canvas.getForeground(); }
+ public void setForeground(Color c) { }
+ public Cursor getCursor() { return canvas.getCursor(); }
+ public void setCursor(Cursor c) { }
+ public Font getFont() { return canvas.getFont(); }
+ public void setFont(Font f) { }
+ public FontMetrics getFontMetrics(Font f) { return canvas.getFontMetrics(f); }
+ public boolean isEnabled() { return node() != null && !Boolean.FALSE.equals(node().getEnabled()); }
+ public void setEnabled(boolean b) { }
+ public boolean isVisible() { return node() != null && node().getBounds().getWidth() > 0 && node().getBounds().getHeight() > 0; }
+ public void setVisible(boolean b) { }
+ public boolean isShowing() { return canvas.isShowing() && isVisible(); }
+ public boolean contains(Point p) { return getBounds().contains(p); }
+ public Point getLocationOnScreen() {
+ Point rootLocation = canvas.getLocationOnScreen();
+ Rectangle bounds = getBounds();
+ return new Point(rootLocation.x + bounds.x, rootLocation.y + bounds.y);
+ }
+ public Point getLocation() { Rectangle b = getBounds(); return new Point(b.x, b.y); }
+ public void setLocation(Point p) { }
+ public Rectangle getBounds() {
+ if (node() == null) return new Rectangle();
+ com.codename1.ui.geom.Rectangle b = node().getBounds();
+ return new Rectangle(b.getX(), b.getY(), b.getWidth(), b.getHeight());
+ }
+ public void setBounds(Rectangle r) { }
+ public Dimension getSize() { Rectangle b = getBounds(); return new Dimension(b.width, b.height); }
+ public void setSize(Dimension d) { }
+ public Accessible getAccessibleAt(Point p) {
+ AccessibilityNodeSnapshot n = tree().getNodeAt(p.x, p.y);
+ return n == null ? null : accessible(n.getId());
+ }
+ public boolean isFocusTraversable() { return node() != null && node().isFocusable(); }
+ public void requestFocus() { implementation.performAccessibilityAction(id, AccessibilityAction.FOCUS, null); }
+ public void addFocusListener(FocusListener l) { canvas.addFocusListener(l); }
+ public void removeFocusListener(FocusListener l) { canvas.removeFocusListener(l); }
+ }
+ }
+
+ private AccessibleStateSet states(AccessibilityNodeSnapshot node) {
+ AccessibleStateSet states = new AccessibleStateSet();
+ if (node == null) return states;
+ if (!Boolean.FALSE.equals(node.getEnabled())) states.add(AccessibleState.ENABLED);
+ if (node.getBounds().getWidth() > 0 && node.getBounds().getHeight() > 0) {
+ states.add(AccessibleState.VISIBLE);
+ if (canvas.isShowing()) states.add(AccessibleState.SHOWING);
+ }
+ if (node.isFocusable()) states.add(AccessibleState.FOCUSABLE);
+ if (node.isFocused()) states.add(AccessibleState.FOCUSED);
+ if (Boolean.TRUE.equals(node.getSelected())) states.add(AccessibleState.SELECTED);
+ if (node.getChecked() == AccessibilityCheckedState.CHECKED) states.add(AccessibleState.CHECKED);
+ if (Boolean.TRUE.equals(node.getExpanded())) states.add(AccessibleState.EXPANDED);
+ if (Boolean.FALSE.equals(node.getExpanded())) states.add(AccessibleState.COLLAPSED);
+ if (Boolean.TRUE.equals(node.getBusy())) states.add(AccessibleState.BUSY);
+ if (!Boolean.TRUE.equals(node.getReadOnly()) && (node.getRole() == AccessibilityRole.TEXT_FIELD
+ || node.getRole() == AccessibilityRole.SEARCH_FIELD)) states.add(AccessibleState.EDITABLE);
+ if (Boolean.TRUE.equals(node.getMultiline())) states.add(AccessibleState.MULTI_LINE);
+ return states;
+ }
+
+ private AccessibleRole role(AccessibilityRole role) {
+ switch (role) {
+ case BUTTON: return AccessibleRole.PUSH_BUTTON;
+ case TOGGLE_BUTTON: return AccessibleRole.TOGGLE_BUTTON;
+ case CHECKBOX: return AccessibleRole.CHECK_BOX;
+ case RADIO_BUTTON: return AccessibleRole.RADIO_BUTTON;
+ case SWITCH: return AccessibleRole.TOGGLE_BUTTON;
+ case HEADING:
+ case STATIC_TEXT: return AccessibleRole.LABEL;
+ case LINK: return AccessibleRole.HYPERLINK;
+ case IMAGE: return AccessibleRole.ICON;
+ case TEXT_FIELD:
+ case SEARCH_FIELD: return AccessibleRole.TEXT;
+ case SLIDER: return AccessibleRole.SLIDER;
+ case PROGRESS_BAR: return AccessibleRole.PROGRESS_BAR;
+ case LIST: return AccessibleRole.LIST;
+ case LIST_ITEM: return AccessibleRole.LIST_ITEM;
+ case GRID: return AccessibleRole.TABLE;
+ case CELL: return AccessibleRole.UNKNOWN;
+ case TAB_LIST: return AccessibleRole.PAGE_TAB_LIST;
+ case TAB: return AccessibleRole.PAGE_TAB;
+ case DIALOG: return AccessibleRole.DIALOG;
+ case ALERT: return AccessibleRole.ALERT;
+ case MENU: return AccessibleRole.MENU;
+ case MENU_ITEM: return AccessibleRole.MENU_ITEM;
+ case TOOLBAR: return AccessibleRole.TOOL_BAR;
+ case SCROLL_BAR: return AccessibleRole.SCROLL_BAR;
+ case COMBO_BOX: return AccessibleRole.COMBO_BOX;
+ case TREE: return AccessibleRole.TREE;
+ case SEPARATOR: return AccessibleRole.SEPARATOR;
+ default: return AccessibleRole.PANEL;
+ }
+ }
+}
diff --git a/Ports/JavaSE/src/com/codename1/impl/javase/JavaSEPort.java b/Ports/JavaSE/src/com/codename1/impl/javase/JavaSEPort.java
index cd310f49673..e9572232cfd 100644
--- a/Ports/JavaSE/src/com/codename1/impl/javase/JavaSEPort.java
+++ b/Ports/JavaSE/src/com/codename1/impl/javase/JavaSEPort.java
@@ -2456,6 +2456,7 @@ public int getCanvasY() {
}
protected class C extends JPanel implements KeyListener, MouseListener, MouseMotionListener, HierarchyBoundsListener, AdjustmentListener, MouseWheelListener {
+ private JavaSEAccessibility cn1Accessibility;
private BufferedImage buffer;
boolean painted;
private Graphics2D g2dInstance;
@@ -2490,6 +2491,18 @@ public void hierarchyChanged(HierarchyEvent e) {
requestFocus();
}
+ @Override
+ public javax.accessibility.AccessibleContext getAccessibleContext() {
+ if (cn1Accessibility == null) {
+ cn1Accessibility = new JavaSEAccessibility(this, JavaSEPort.this);
+ }
+ return cn1Accessibility.getContext();
+ }
+
+ void accessibilityChanged(int changeType) {
+ if (cn1Accessibility != null) cn1Accessibility.changed(changeType);
+ }
+
private void installNativeMagnificationListeners() {
installMagnificationWheelFallbackListener();
installNativeMagnificationListener(this);
@@ -3767,6 +3780,20 @@ public JavaSEPort() {
instance = this;
}
+ @Override
+ public void accessibilityTreeChanged(final int changeType) {
+ EventQueue.invokeLater(new Runnable() {
+ public void run() {
+ if (canvas != null) canvas.accessibilityChanged(changeType);
+ }
+ });
+ }
+
+ @Override
+ public boolean isAccessibilityTreeSupported() {
+ return true;
+ }
+
public void paintDirty() {
super.paintDirty();
}
diff --git a/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/HTML5Implementation.java b/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/HTML5Implementation.java
index 4048f72fce7..9fb7ee00561 100644
--- a/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/HTML5Implementation.java
+++ b/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/HTML5Implementation.java
@@ -87,6 +87,14 @@
import com.codename1.ui.events.FocusListener;
import com.codename1.ui.events.MessageEvent;
import com.codename1.ui.geom.Rectangle;
+import com.codename1.ui.accessibility.AccessibilityAction;
+import com.codename1.ui.accessibility.AccessibilityCheckedState;
+import com.codename1.ui.accessibility.AccessibilityCollectionItemInfo;
+import com.codename1.ui.accessibility.AccessibilityLiveRegion;
+import com.codename1.ui.accessibility.AccessibilityNodeSnapshot;
+import com.codename1.ui.accessibility.AccessibilityRange;
+import com.codename1.ui.accessibility.AccessibilityRole;
+import com.codename1.ui.accessibility.AccessibilityTreeSnapshot;
import com.codename1.ui.geom.Shape;
import com.codename1.ui.layouts.BorderLayout;
import com.codename1.ui.layouts.BoxLayout;
@@ -571,6 +579,7 @@ private boolean hitTest(int x, int y) {
* Container for all peer components.
*/
HTMLElement peersContainer;
+ private HTMLElement accessibilityContainer;
/**
@@ -1255,6 +1264,13 @@ private void __init() {
peersContainer = (HTMLElement)document.createElement("div");
peersContainer.setAttribute("id", "cn1-peers-container");
outputCanvas.getParentNode().insertBefore(peersContainer, outputCanvas);
+ accessibilityContainer = (HTMLElement)document.createElement("div");
+ accessibilityContainer.setAttribute("id", "cn1-accessibility-tree");
+ accessibilityContainer.setAttribute("role", "application");
+ accessibilityContainer.getStyle().setCssText("position:absolute;left:0;top:0;width:100%;height:100%;overflow:hidden;pointer-events:none;z-index:2147483646;");
+ outputCanvas.getParentNode().insertBefore(accessibilityContainer, outputCanvas);
+ outputCanvas.setAttribute("role", "presentation");
+ outputCanvas.setAttribute("aria-hidden", "true");
nativeEdt = EasyThread.start("NativeEDT");
@@ -2854,6 +2870,191 @@ public void revalidate() {
}
}
+
+ @Override
+ public void accessibilityTreeChanged(int changeType) {
+ if (accessibilityContainer == null) return;
+ AccessibilityTreeSnapshot tree = getAccessibilityTreeSnapshot();
+ accessibilityContainer.setInnerHTML("");
+ final Map elements = new HashMap();
+ for (AccessibilityNodeSnapshot node : tree.getNodes().values()) {
+ final long nodeId = node.getId();
+ final HTMLElement element = (HTMLElement)document.createElement("div");
+ elements.put(Long.valueOf(nodeId), element);
+ element.setAttribute("data-cn1-accessibility-id", String.valueOf(nodeId));
+ applyAria(element, node);
+ if (node.getParentId() >= 0 && elements.get(Long.valueOf(node.getParentId())) != null) {
+ elements.get(Long.valueOf(node.getParentId())).appendChild(element);
+ } else {
+ accessibilityContainer.appendChild(element);
+ }
+ final AccessibilityAction activate = node.getAction(AccessibilityAction.ACTIVATE);
+ if (activate != null && activate.isEnabled()) {
+ element.addEventListener("click", new EventListener() {
+ public void handleEvent(Event event) {
+ event.preventDefault();
+ event.stopPropagation();
+ performAccessibilityAction(nodeId, AccessibilityAction.ACTIVATE, null);
+ }
+ });
+ }
+ final AccessibilityAction focus = node.getAction(AccessibilityAction.FOCUS);
+ if (focus != null && focus.isEnabled()) {
+ element.addEventListener("focus", new EventListener() {
+ public void handleEvent(Event event) {
+ performAccessibilityAction(nodeId, AccessibilityAction.FOCUS, null);
+ }
+ });
+ }
+ element.addEventListener("keydown", new EventListener() {
+ public void handleEvent(Event event) {
+ JSOImplementations.KeyEvent key = (JSOImplementations.KeyEvent)event;
+ int code = key.getKeyCode();
+ if ((code == 13 || code == 32) && activate != null) {
+ event.preventDefault();
+ performAccessibilityAction(nodeId, AccessibilityAction.ACTIVATE, null);
+ } else if (code == 38 || code == 39) {
+ performAccessibilityAction(nodeId, AccessibilityAction.INCREMENT, null);
+ } else if (code == 37 || code == 40) {
+ performAccessibilityAction(nodeId, AccessibilityAction.DECREMENT, null);
+ }
+ }
+ });
+ addWebCustomActions(element, node);
+ }
+ }
+
+ @Override
+ public boolean isAccessibilityTreeSupported() {
+ return true;
+ }
+
+ private void applyAria(HTMLElement element, AccessibilityNodeSnapshot node) {
+ String role = ariaRole(node.getRole());
+ if (role != null) element.setAttribute("role", role);
+ if (node.getLabel() != null) element.setAttribute("aria-label", node.getLabel());
+ String description = node.getDescription();
+ if (node.getHint() != null) description = description == null ? node.getHint() : description + ". " + node.getHint();
+ if (node.getValidationError() != null) description = description == null ? node.getValidationError() : description + ". " + node.getValidationError();
+ if (description != null) element.setAttribute("aria-description", description);
+ if (node.getIdentifier() != null) element.setAttribute("id", node.getIdentifier());
+ if (node.getRoleDescription() != null) element.setAttribute("aria-roledescription", node.getRoleDescription());
+ if (node.getValue() != null) element.setAttribute("aria-valuetext", node.getValue());
+ if (node.getSelected() != null) element.setAttribute("aria-selected", String.valueOf(node.getSelected()));
+ if (node.getExpanded() != null) element.setAttribute("aria-expanded", String.valueOf(node.getExpanded()));
+ if (node.getEnabled() != null && !node.getEnabled().booleanValue()) element.setAttribute("aria-disabled", "true");
+ if (node.getInvalid() != null) element.setAttribute("aria-invalid", String.valueOf(node.getInvalid()));
+ if (node.getBusy() != null) element.setAttribute("aria-busy", String.valueOf(node.getBusy()));
+ if (node.getReadOnly() != null) element.setAttribute("aria-readonly", String.valueOf(node.getReadOnly()));
+ if (node.getRequired() != null) element.setAttribute("aria-required", String.valueOf(node.getRequired()));
+ if (node.getMultiline() != null) element.setAttribute("aria-multiline", String.valueOf(node.getMultiline()));
+ if (node.getCurrent() != null && node.getCurrent().booleanValue()) element.setAttribute("aria-current", "true");
+ if (node.isModal()) element.setAttribute("aria-modal", "true");
+ if (node.getHeadingLevel() > 0) element.setAttribute("aria-level", String.valueOf(node.getHeadingLevel()));
+ if (node.getChecked() != AccessibilityCheckedState.UNSPECIFIED) {
+ element.setAttribute("aria-checked", node.getChecked() == AccessibilityCheckedState.MIXED
+ ? "mixed" : String.valueOf(node.getChecked() == AccessibilityCheckedState.CHECKED));
+ }
+ if (node.getPressed() != null) element.setAttribute("aria-pressed", String.valueOf(node.getPressed()));
+ if (node.getLiveRegion() != AccessibilityLiveRegion.OFF) {
+ element.setAttribute("aria-live", node.getLiveRegion() == AccessibilityLiveRegion.ASSERTIVE ? "assertive" : "polite");
+ element.setAttribute("aria-atomic", "true");
+ }
+ AccessibilityRange range = node.getRange();
+ if (range != null) {
+ element.setAttribute("aria-valuemin", String.valueOf(range.getMinimum()));
+ element.setAttribute("aria-valuemax", String.valueOf(range.getMaximum()));
+ element.setAttribute("aria-valuenow", String.valueOf(range.getCurrent()));
+ if (range.getText() != null) element.setAttribute("aria-valuetext", range.getText());
+ }
+ AccessibilityCollectionItemInfo item = node.getCollectionItemInfo();
+ if (item != null) {
+ if (item.getPositionInSet() > 0) element.setAttribute("aria-posinset", String.valueOf(item.getPositionInSet()));
+ if (item.getSetSize() != 0) element.setAttribute("aria-setsize", String.valueOf(item.getSetSize()));
+ if (item.getLevel() > 0) element.setAttribute("aria-level", String.valueOf(item.getLevel()));
+ if (item.getRowIndex() >= 0) element.setAttribute("aria-rowindex", String.valueOf(item.getRowIndex() + 1));
+ if (item.getColumnIndex() >= 0) element.setAttribute("aria-colindex", String.valueOf(item.getColumnIndex() + 1));
+ if (item.getRowSpan() > 1) element.setAttribute("aria-rowspan", String.valueOf(item.getRowSpan()));
+ if (item.getColumnSpan() > 1) element.setAttribute("aria-colspan", String.valueOf(item.getColumnSpan()));
+ }
+ Rectangle bounds = node.getBounds();
+ double ratio = getDevicePixelRatio();
+ element.getStyle().setCssText("position:absolute;opacity:0.001;pointer-events:none;overflow:hidden;"
+ + "left:" + bounds.getX() / ratio + "px;top:" + bounds.getY() / ratio + "px;"
+ + "width:" + Math.max(1, bounds.getWidth()) / ratio + "px;height:"
+ + Math.max(1, bounds.getHeight()) / ratio + "px;");
+ if (node.isFocusable()) element.setTabIndex(0);
+ else element.setTabIndex(-1);
+ if (node.getRole() == AccessibilityRole.STATIC_TEXT || node.getRole() == AccessibilityRole.HEADING) {
+ element.setTextContent(node.getLabel() == null ? "" : node.getLabel());
+ }
+ }
+
+ private void addWebCustomActions(HTMLElement parent, AccessibilityNodeSnapshot node) {
+ for (final AccessibilityAction action : node.getActions()) {
+ if (!action.isEnabled() || isStandardWebAction(action.getId())) continue;
+ final long nodeId = node.getId();
+ HTMLElement button = (HTMLElement)document.createElement("button");
+ button.setAttribute("type", "button");
+ button.setAttribute("aria-label", action.getLabel() == null ? action.getId() : action.getLabel());
+ button.setTextContent(action.getLabel() == null ? action.getId() : action.getLabel());
+ button.getStyle().setCssText("position:absolute;opacity:0.001;pointer-events:none;width:1px;height:1px;");
+ button.addEventListener("click", new EventListener() {
+ public void handleEvent(Event event) {
+ event.preventDefault();
+ performAccessibilityAction(nodeId, action.getId(), null);
+ }
+ });
+ parent.appendChild(button);
+ }
+ }
+
+ private boolean isStandardWebAction(String id) {
+ return AccessibilityAction.ACTIVATE.equals(id) || AccessibilityAction.FOCUS.equals(id)
+ || AccessibilityAction.INCREMENT.equals(id) || AccessibilityAction.DECREMENT.equals(id)
+ || AccessibilityAction.SET_TEXT.equals(id) || AccessibilityAction.SCROLL_FORWARD.equals(id)
+ || AccessibilityAction.SCROLL_BACKWARD.equals(id);
+ }
+
+ private String ariaRole(AccessibilityRole role) {
+ switch (role) {
+ case BUTTON:
+ case TOGGLE_BUTTON: return "button";
+ case CHECKBOX: return "checkbox";
+ case RADIO_BUTTON: return "radio";
+ case SWITCH: return "switch";
+ case HEADING: return "heading";
+ case LINK: return "link";
+ case IMAGE: return "img";
+ case TEXT_FIELD: return "textbox";
+ case SEARCH_FIELD: return "searchbox";
+ case SLIDER: return "slider";
+ case PROGRESS_BAR: return "progressbar";
+ case LIST: return "list";
+ case LIST_ITEM: return "listitem";
+ case GRID: return "grid";
+ case ROW: return "row";
+ case CELL: return "gridcell";
+ case COLUMN_HEADER: return "columnheader";
+ case ROW_HEADER: return "rowheader";
+ case TAB_LIST: return "tablist";
+ case TAB: return "tab";
+ case TAB_PANEL: return "tabpanel";
+ case DIALOG: return "dialog";
+ case ALERT: return "alert";
+ case MENU: return "menu";
+ case MENU_ITEM: return "menuitem";
+ case TOOLBAR: return "toolbar";
+ case SCROLL_BAR: return "scrollbar";
+ case SPIN_BUTTON: return "spinbutton";
+ case COMBO_BOX: return "combobox";
+ case TREE: return "tree";
+ case TREE_ITEM: return "treeitem";
+ case SEPARATOR: return "separator";
+ case GENERIC: return "group";
+ default: return null;
+ }
+ }
public static void setMainClass(Object main) {
JavaScriptBootstrapCoordinator.bindMainClass(main,
diff --git a/Ports/LinuxPort/nativeSources/cn1_linux.h b/Ports/LinuxPort/nativeSources/cn1_linux.h
index 97aa5f4a8a7..7a1de5a5268 100644
--- a/Ports/LinuxPort/nativeSources/cn1_linux.h
+++ b/Ports/LinuxPort/nativeSources/cn1_linux.h
@@ -69,7 +69,8 @@ typedef enum {
* ROTATE. drainInput decodes these into Display.fireMagnifyGesture /
* fireRotationGesture, the same hooks the macOS trackpad drives. */
CN1_EVENT_PINCH = 10,
- CN1_EVENT_ROTATE = 11
+ CN1_EVENT_ROTATE = 11,
+ CN1_EVENT_ACCESSIBILITY_ACTION = 12
} CN1EventType;
/* Fixed-point scale for the gesture keyCode field (see CN1_EVENT_PINCH). */
diff --git a/Ports/LinuxPort/nativeSources/cn1_linux_window.c b/Ports/LinuxPort/nativeSources/cn1_linux_window.c
index 8e5d032e3bd..b21b0a0c3a4 100644
--- a/Ports/LinuxPort/nativeSources/cn1_linux_window.c
+++ b/Ports/LinuxPort/nativeSources/cn1_linux_window.c
@@ -95,6 +95,7 @@ static GtkWidget* cn1Window = 0;
static GtkWidget* cn1DrawingArea = 0;
static GtkWidget* cn1Overlay = 0; /* GtkOverlay: drawing area + native widget layer */
static GtkWidget* cn1Fixed = 0; /* GtkFixed overlay hosting positioned native peers */
+static GtkWidget* cn1AccessibilityFixed = 0; /* transparent GTK/ATK semantic hierarchy */
static CN1Graphics cn1WindowG; /* the on-screen / headless back buffer */
static int cn1DisplayWidth = 800;
static int cn1DisplayHeight = 600;
@@ -602,6 +603,10 @@ JAVA_VOID com_codename1_impl_linux_LinuxNative_initDisplay___java_lang_String_in
cn1Fixed = gtk_fixed_new();
gtk_overlay_add_overlay(GTK_OVERLAY(cn1Overlay), cn1Fixed);
gtk_overlay_set_overlay_pass_through(GTK_OVERLAY(cn1Overlay), cn1Fixed, TRUE);
+ cn1AccessibilityFixed = gtk_fixed_new();
+ gtk_widget_set_opacity(cn1AccessibilityFixed, 0.01);
+ gtk_overlay_add_overlay(GTK_OVERLAY(cn1Overlay), cn1AccessibilityFixed);
+ gtk_overlay_set_overlay_pass_through(GTK_OVERLAY(cn1Overlay), cn1AccessibilityFixed, TRUE);
gtk_container_add(GTK_CONTAINER(cn1Window), cn1Overlay);
g_signal_connect(cn1DrawingArea, "draw", G_CALLBACK(cn1OnDraw), 0);
@@ -714,6 +719,223 @@ JAVA_BOOLEAN com_codename1_impl_linux_LinuxNative_pollEvent___int_1ARRAY_R_boole
return JAVA_FALSE;
}
+/* ------------------------------------------------ accessibility / AT-SPI */
+
+typedef struct CN1A11yEntry {
+ long long id;
+ long long parentId;
+ GtkWidget* widget;
+ int x, y;
+ struct CN1A11yEntry* next;
+} CN1A11yEntry;
+
+typedef struct {
+ long long nodeId;
+ int actionHash;
+ char* actionId;
+ char* label;
+} CN1A11yAction;
+
+static CN1A11yEntry* cn1A11yEntries = 0;
+
+static void cn1A11yFreeEntries(void) {
+ CN1A11yEntry* entry = cn1A11yEntries;
+ while (entry) {
+ CN1A11yEntry* next = entry->next;
+ free(entry);
+ entry = next;
+ }
+ cn1A11yEntries = 0;
+}
+
+static CN1A11yEntry* cn1A11yFind(long long id) {
+ CN1A11yEntry* entry;
+ for (entry = cn1A11yEntries; entry; entry = entry->next) {
+ if (entry->id == id) return entry;
+ }
+ return 0;
+}
+
+static AtkRole cn1A11yRole(const char* role) {
+ if (!role) return ATK_ROLE_PANEL;
+ if (!strcmp(role, "BUTTON") || !strcmp(role, "TOGGLE_BUTTON")) return ATK_ROLE_PUSH_BUTTON;
+ if (!strcmp(role, "CHECKBOX")) return ATK_ROLE_CHECK_BOX;
+ if (!strcmp(role, "RADIO_BUTTON")) return ATK_ROLE_RADIO_BUTTON;
+ if (!strcmp(role, "SWITCH")) return ATK_ROLE_TOGGLE_BUTTON;
+ if (!strcmp(role, "HEADING")) return ATK_ROLE_HEADING;
+ if (!strcmp(role, "LINK")) return ATK_ROLE_LINK;
+ if (!strcmp(role, "IMAGE")) return ATK_ROLE_IMAGE;
+ if (!strcmp(role, "STATIC_TEXT")) return ATK_ROLE_LABEL;
+ if (!strcmp(role, "TEXT_FIELD") || !strcmp(role, "SEARCH_FIELD")) return ATK_ROLE_ENTRY;
+ if (!strcmp(role, "SLIDER")) return ATK_ROLE_SLIDER;
+ if (!strcmp(role, "PROGRESS_BAR")) return ATK_ROLE_PROGRESS_BAR;
+ if (!strcmp(role, "LIST")) return ATK_ROLE_LIST;
+ if (!strcmp(role, "LIST_ITEM")) return ATK_ROLE_LIST_ITEM;
+ if (!strcmp(role, "GRID")) return ATK_ROLE_TABLE;
+ if (!strcmp(role, "ROW")) return ATK_ROLE_TABLE_ROW;
+ if (!strcmp(role, "CELL")) return ATK_ROLE_TABLE_CELL;
+ if (!strcmp(role, "COLUMN_HEADER")) return ATK_ROLE_COLUMN_HEADER;
+ if (!strcmp(role, "ROW_HEADER")) return ATK_ROLE_ROW_HEADER;
+ if (!strcmp(role, "TAB_LIST")) return ATK_ROLE_PAGE_TAB_LIST;
+ if (!strcmp(role, "TAB")) return ATK_ROLE_PAGE_TAB;
+ if (!strcmp(role, "DIALOG")) return ATK_ROLE_DIALOG;
+ if (!strcmp(role, "ALERT")) return ATK_ROLE_ALERT;
+ if (!strcmp(role, "MENU")) return ATK_ROLE_MENU;
+ if (!strcmp(role, "MENU_ITEM")) return ATK_ROLE_MENU_ITEM;
+ if (!strcmp(role, "TOOLBAR")) return ATK_ROLE_TOOL_BAR;
+ if (!strcmp(role, "SCROLL_BAR")) return ATK_ROLE_SCROLL_BAR;
+ if (!strcmp(role, "COMBO_BOX")) return ATK_ROLE_COMBO_BOX;
+ if (!strcmp(role, "TREE")) return ATK_ROLE_TREE;
+ if (!strcmp(role, "TREE_ITEM")) return ATK_ROLE_TREE_ITEM;
+ if (!strcmp(role, "SEPARATOR")) return ATK_ROLE_SEPARATOR;
+ return ATK_ROLE_PANEL;
+}
+
+static void cn1A11yBeginMain(void* ignored) {
+ (void) ignored;
+ cn1A11yFreeEntries();
+ if (cn1AccessibilityFixed) {
+ GList* children = gtk_container_get_children(GTK_CONTAINER(cn1AccessibilityFixed));
+ GList* item;
+ for (item = children; item; item = item->next) {
+ gtk_widget_destroy(GTK_WIDGET(item->data));
+ }
+ g_list_free(children);
+ }
+}
+
+JAVA_VOID com_codename1_impl_linux_LinuxNative_accessibilityBegin__(CODENAME_ONE_THREAD_STATE) {
+ cn1LinuxRunOnMainAndWait(cn1A11yBeginMain, 0);
+}
+
+typedef struct {
+ long long id, parentId;
+ char *role, *label, *description, *value;
+ int x, y, width, height, flags;
+} CN1A11yNodeCall;
+
+static GtkWidget* cn1A11yWidget(const char* role, const char* label, const char* value, int flags) {
+ GtkWidget* widget;
+ if (role && (!strcmp(role, "BUTTON") || !strcmp(role, "TOGGLE_BUTTON") || !strcmp(role, "SWITCH"))) {
+ widget = gtk_toggle_button_new_with_label(label ? label : "");
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(widget), (flags & 0x30) == 0x20);
+ gtk_toggle_button_set_inconsistent(GTK_TOGGLE_BUTTON(widget), (flags & 0x30) == 0x30);
+ } else if (role && (!strcmp(role, "CHECKBOX") || !strcmp(role, "RADIO_BUTTON"))) {
+ widget = gtk_check_button_new_with_label(label ? label : "");
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(widget), (flags & 0x30) == 0x20);
+ gtk_toggle_button_set_inconsistent(GTK_TOGGLE_BUTTON(widget), (flags & 0x30) == 0x30);
+ } else if (role && (!strcmp(role, "TEXT_FIELD") || !strcmp(role, "SEARCH_FIELD"))) {
+ widget = gtk_entry_new();
+ if (value) gtk_entry_set_text(GTK_ENTRY(widget), value);
+ } else if (role && (!strcmp(role, "SLIDER") || !strcmp(role, "PROGRESS_BAR"))) {
+ widget = gtk_scale_new_with_range(GTK_ORIENTATION_HORIZONTAL, 0, 100, 1);
+ if (value) gtk_range_set_value(GTK_RANGE(widget), atof(value));
+ } else {
+ widget = gtk_fixed_new();
+ }
+ return widget;
+}
+
+static void cn1A11yNodeMain(void* pointer) {
+ CN1A11yNodeCall* call = (CN1A11yNodeCall*) pointer;
+ CN1A11yEntry* parent = cn1A11yFind(call->parentId);
+ GtkWidget* host = parent ? parent->widget : cn1AccessibilityFixed;
+ GtkWidget* widget = cn1A11yWidget(call->role, call->label, call->value, call->flags);
+ AtkObject* accessible = gtk_widget_get_accessible(widget);
+ atk_object_set_role(accessible, cn1A11yRole(call->role));
+ if (call->label) atk_object_set_name(accessible, call->label);
+ if (call->description) atk_object_set_description(accessible, call->description);
+ gtk_widget_set_can_focus(widget, (call->flags & 1) != 0);
+ gtk_widget_set_sensitive(widget, (call->flags & 4) != 0);
+ gtk_widget_set_size_request(widget, call->width > 0 ? call->width : 1,
+ call->height > 0 ? call->height : 1);
+ if (host && GTK_IS_FIXED(host)) {
+ int px = parent ? call->x - parent->x : call->x;
+ int py = parent ? call->y - parent->y : call->y;
+ gtk_fixed_put(GTK_FIXED(host), widget, px, py);
+ } else if (host && GTK_IS_CONTAINER(host)) {
+ gtk_container_add(GTK_CONTAINER(host), widget);
+ }
+ gtk_widget_show_all(widget);
+ CN1A11yEntry* entry = (CN1A11yEntry*) calloc(1, sizeof(CN1A11yEntry));
+ entry->id = call->id; entry->parentId = call->parentId; entry->widget = widget;
+ entry->x = call->x; entry->y = call->y; entry->next = cn1A11yEntries; cn1A11yEntries = entry;
+ free(call->role); free(call->label); free(call->description); free(call->value); free(call);
+}
+
+JAVA_VOID com_codename1_impl_linux_LinuxNative_accessibilityNode___long_long_java_lang_String_java_lang_String_java_lang_String_java_lang_String_int_int_int_int_int(
+ CODENAME_ONE_THREAD_STATE, JAVA_LONG id, JAVA_LONG parentId, JAVA_OBJECT role, JAVA_OBJECT label,
+ JAVA_OBJECT description, JAVA_OBJECT value, JAVA_INT x, JAVA_INT y, JAVA_INT width, JAVA_INT height,
+ JAVA_INT flags) {
+ extern const char* stringToUTF8(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT);
+ CN1A11yNodeCall* call = (CN1A11yNodeCall*) calloc(1, sizeof(CN1A11yNodeCall));
+ call->id = id; call->parentId = parentId; call->x = x; call->y = y;
+ call->width = width; call->height = height; call->flags = flags;
+ call->role = role == JAVA_NULL ? 0 : strdup(stringToUTF8(threadStateData, role));
+ call->label = label == JAVA_NULL ? 0 : strdup(stringToUTF8(threadStateData, label));
+ call->description = description == JAVA_NULL ? 0 : strdup(stringToUTF8(threadStateData, description));
+ call->value = value == JAVA_NULL ? 0 : strdup(stringToUTF8(threadStateData, value));
+ cn1LinuxRunOnMainAndWait(cn1A11yNodeMain, call);
+}
+
+static void cn1A11yActionActivated(GtkWidget* widget, gpointer pointer) {
+ CN1A11yAction* action = (CN1A11yAction*) pointer;
+ (void) widget;
+ cn1LinuxPushEvent(CN1_EVENT_ACCESSIBILITY_ACTION, (int) action->nodeId, 0, action->actionHash);
+}
+
+static void cn1A11yActionFree(gpointer pointer, GClosure* closure) {
+ CN1A11yAction* action = (CN1A11yAction*) pointer;
+ (void) closure;
+ free(action->actionId); free(action->label); free(action);
+}
+
+static void cn1A11yActionMain(void* pointer) {
+ CN1A11yAction* action = (CN1A11yAction*) pointer;
+ CN1A11yEntry* entry = cn1A11yFind(action->nodeId);
+ if (!entry) { cn1A11yActionFree(action, 0); return; }
+ if (!strcmp(action->actionId, "activate") || !strcmp(action->actionId, "focus")) {
+ if (GTK_IS_BUTTON(entry->widget)) {
+ g_signal_connect_data(entry->widget, "clicked", G_CALLBACK(cn1A11yActionActivated), action,
+ cn1A11yActionFree, 0);
+ return;
+ }
+ } else {
+ GtkWidget* custom = gtk_button_new_with_label(action->label ? action->label : action->actionId);
+ AtkObject* accessible = gtk_widget_get_accessible(custom);
+ atk_object_set_name(accessible, action->label ? action->label : action->actionId);
+ atk_object_set_description(accessible, "Custom accessibility action");
+ gtk_widget_set_size_request(custom, entry->widget ? gtk_widget_get_allocated_width(entry->widget) : 1,
+ entry->widget ? gtk_widget_get_allocated_height(entry->widget) : 1);
+ gtk_fixed_put(GTK_FIXED(cn1AccessibilityFixed), custom, entry->x, entry->y);
+ gtk_widget_show(custom);
+ g_signal_connect_data(custom, "clicked", G_CALLBACK(cn1A11yActionActivated), action,
+ cn1A11yActionFree, 0);
+ return;
+ }
+ cn1A11yActionFree(action, 0);
+}
+
+JAVA_VOID com_codename1_impl_linux_LinuxNative_accessibilityAction___long_java_lang_String_int_java_lang_String(
+ CODENAME_ONE_THREAD_STATE, JAVA_LONG nodeId, JAVA_OBJECT actionId, JAVA_INT actionHash, JAVA_OBJECT label) {
+ extern const char* stringToUTF8(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT);
+ CN1A11yAction* action = (CN1A11yAction*) calloc(1, sizeof(CN1A11yAction));
+ action->nodeId = nodeId; action->actionHash = actionHash;
+ action->actionId = actionId == JAVA_NULL ? strdup("") : strdup(stringToUTF8(threadStateData, actionId));
+ action->label = label == JAVA_NULL ? 0 : strdup(stringToUTF8(threadStateData, label));
+ cn1LinuxRunOnMainAndWait(cn1A11yActionMain, action);
+}
+
+static void cn1A11yEndMain(void* pointer) {
+ (void) pointer;
+ if (cn1AccessibilityFixed) gtk_widget_show_all(cn1AccessibilityFixed);
+}
+
+JAVA_VOID com_codename1_impl_linux_LinuxNative_accessibilityEnd___int(CODENAME_ONE_THREAD_STATE, JAVA_INT changeType) {
+ (void) changeType;
+ cn1LinuxRunOnMainAndWait(cn1A11yEndMain, 0);
+}
+
JAVA_BOOLEAN com_codename1_impl_linux_LinuxNative_pumpMessages___R_boolean(CODENAME_ONE_THREAD_STATE) {
/* Process all pending GTK events, then block briefly for the next one so the
* loop yields the CPU. Returns false once the window has been closed. */
diff --git a/Ports/LinuxPort/src/com/codename1/impl/linux/LinuxImplementation.java b/Ports/LinuxPort/src/com/codename1/impl/linux/LinuxImplementation.java
index 0ad7ab4eda3..53a4fc02dcb 100644
--- a/Ports/LinuxPort/src/com/codename1/impl/linux/LinuxImplementation.java
+++ b/Ports/LinuxPort/src/com/codename1/impl/linux/LinuxImplementation.java
@@ -35,6 +35,10 @@
import com.codename1.ui.Stroke;
import com.codename1.ui.geom.PathIterator;
import com.codename1.ui.geom.Shape;
+import com.codename1.ui.accessibility.AccessibilityAction;
+import com.codename1.ui.accessibility.AccessibilityManager;
+import com.codename1.ui.accessibility.AccessibilityNodeSnapshot;
+import com.codename1.ui.accessibility.AccessibilityTreeSnapshot;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ByteArrayInputStream;
@@ -80,6 +84,7 @@ public class LinuxImplementation extends CodenameOneImplementation {
private static final int EVENT_MOUSE_HWHEEL = 9;
private static final int EVENT_PINCH = 10;
private static final int EVENT_ROTATE = 11;
+ private static final int EVENT_ACCESSIBILITY_ACTION = 12;
// The native gesture events encode their float (incremental scale / radians) as
// an int in 1/10000 units; see CN1_GESTURE_FIXED in cn1_linux.h.
@@ -681,6 +686,9 @@ private void drainInput() {
case EVENT_CLOSE:
Display.getInstance().exitApplication();
break;
+ case EVENT_ACCESSIBILITY_ACTION:
+ AccessibilityManager.getInstance().performActionByHash(x & 0xffffffffL, key, null);
+ break;
default:
break;
}
@@ -693,6 +701,49 @@ private void drainInput() {
}
}
+ @Override
+ public void accessibilityTreeChanged(int changeType) {
+ AccessibilityTreeSnapshot tree = getAccessibilityTreeSnapshot();
+ LinuxNative.accessibilityBegin();
+ for (AccessibilityNodeSnapshot node : tree.getNodes().values()) {
+ int flags = (node.isFocusable() ? 1 : 0) | (node.isFocused() ? 2 : 0)
+ | (Boolean.TRUE.equals(node.getEnabled()) ? 4 : 0)
+ | (Boolean.TRUE.equals(node.getSelected()) ? 8 : 0)
+ | (node.getChecked().ordinal() << 4)
+ | (Boolean.TRUE.equals(node.getExpanded()) ? 64 : 0)
+ | (Boolean.TRUE.equals(node.getInvalid()) ? 128 : 0);
+ com.codename1.ui.geom.Rectangle b = node.getBounds();
+ LinuxNative.accessibilityNode(node.getId(), node.getParentId(), node.getRole().name(), node.getLabel(),
+ joinDescription(node), node.getValue(), b.getX(), b.getY(), b.getWidth(), b.getHeight(), flags);
+ for (AccessibilityAction action : node.getActions()) {
+ if (action.isEnabled()) {
+ LinuxNative.accessibilityAction(node.getId(), action.getId(), action.getId().hashCode(),
+ action.getLabel() == null ? action.getId() : action.getLabel());
+ }
+ }
+ }
+ LinuxNative.accessibilityEnd(changeType);
+ }
+
+ private String joinDescription(AccessibilityNodeSnapshot node) {
+ StringBuilder out = new StringBuilder();
+ if (node.getHint() != null) out.append(node.getHint());
+ if (node.getDescription() != null) {
+ if (out.length() > 0) out.append(". ");
+ out.append(node.getDescription());
+ }
+ if (node.getValidationError() != null) {
+ if (out.length() > 0) out.append(". ");
+ out.append(node.getValidationError());
+ }
+ return out.length() == 0 ? null : out.toString();
+ }
+
+ @Override
+ public boolean isAccessibilityTreeSupported() {
+ return true;
+ }
+
/* --------------------------------------------------- local notifications
* Desktop semantic (mirrors the JavaSE port): while the app runs, a Timer
* fires the notification at its scheduled time and Shell_NotifyIcon shows a
diff --git a/Ports/LinuxPort/src/com/codename1/impl/linux/LinuxNative.java b/Ports/LinuxPort/src/com/codename1/impl/linux/LinuxNative.java
index dd4d719d858..820bc659a3a 100644
--- a/Ports/LinuxPort/src/com/codename1/impl/linux/LinuxNative.java
+++ b/Ports/LinuxPort/src/com/codename1/impl/linux/LinuxNative.java
@@ -145,6 +145,13 @@ public static native void setTransform(long graphics, float m00, float m10, floa
*/
public static native boolean pollEvent(int[] out);
+ /** Rebuilds the GTK/ATK virtual accessibility hierarchy. */
+ public static native void accessibilityBegin();
+ public static native void accessibilityNode(long id, long parentId, String role, String label,
+ String description, String value, int x, int y, int width, int height, int stateFlags);
+ public static native void accessibilityAction(long nodeId, String actionId, int actionHash, String label);
+ public static native void accessibilityEnd(int changeType);
+
/**
* True when the default seat has a touchscreen device, used for
* {@code Display.isTouchScreen()}.
diff --git a/Ports/WindowsPort/nativeSources/cn1_windows.h b/Ports/WindowsPort/nativeSources/cn1_windows.h
index f044e2a0611..b576c550cc7 100644
--- a/Ports/WindowsPort/nativeSources/cn1_windows.h
+++ b/Ports/WindowsPort/nativeSources/cn1_windows.h
@@ -101,7 +101,8 @@ typedef enum {
* ROTATE. drainInput decodes these into Display.fireMagnifyGesture /
* fireRotationGesture, the same hooks the macOS trackpad drives. */
CN1_EVENT_PINCH = 10,
- CN1_EVENT_ROTATE = 11
+ CN1_EVENT_ROTATE = 11,
+ CN1_EVENT_ACCESSIBILITY_ACTION = 12
} CN1EventType;
/* Fixed-point scale for the gesture keyCode field (see CN1_EVENT_PINCH). */
@@ -255,6 +256,7 @@ int cn1WinCreateWindow(const char* utf8Title, int width, int height);
void cn1WinPushEvent(CN1EventType type, int x, int y, int keyCode);
int cn1WinPollEvent(CN1Event* out);
LRESULT CALLBACK cn1WinWndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam);
+LRESULT cn1WinAccessibilityObject(HWND hwnd, WPARAM wParam, LPARAM lParam);
/* BrowserComponent / WebView2 peer (cn1_windows_browser.cpp). The EDT-facing
* native methods marshal each WebView2 operation to the main (pump) thread by
diff --git a/Ports/WindowsPort/nativeSources/cn1_windows_accessibility.cpp b/Ports/WindowsPort/nativeSources/cn1_windows_accessibility.cpp
new file mode 100644
index 00000000000..d70bb3d7976
--- /dev/null
+++ b/Ports/WindowsPort/nativeSources/cn1_windows_accessibility.cpp
@@ -0,0 +1,266 @@
+/* Native virtual accessibility tree for the lightweight Win32 port. */
+#ifdef _WIN32
+
+#include "cn1_windows.h"
+#include
+#include
+#include
+#include
+#include
+#include
+
+#pragma comment(lib, "uiautomationcore.lib")
+#pragma comment(lib, "oleaut32.lib")
+
+struct CN1UiaNode {
+ long long id, parent;
+ long long actionTarget;
+ std::wstring role, label, description, value;
+ int x, y, width, height, flags;
+ std::vector > actions;
+};
+
+static SRWLOCK cn1UiaLock = SRWLOCK_INIT;
+static std::vector cn1UiaNodes;
+static std::vector cn1UiaPending;
+
+static std::wstring cn1UiaWide(const char* value) {
+ if (!value) return std::wstring();
+ int length = MultiByteToWideChar(CP_UTF8, 0, value, -1, NULL, 0);
+ if (length <= 1) return std::wstring();
+ std::vector buffer((size_t) length);
+ MultiByteToWideChar(CP_UTF8, 0, value, -1, &buffer[0], length);
+ return std::wstring(&buffer[0]);
+}
+
+static bool cn1UiaGet(long long id, CN1UiaNode& out) {
+ bool found = false;
+ AcquireSRWLockShared(&cn1UiaLock);
+ for (size_t i = 0; i < cn1UiaNodes.size(); ++i) {
+ if (cn1UiaNodes[i].id == id) { out = cn1UiaNodes[i]; found = true; break; }
+ }
+ ReleaseSRWLockShared(&cn1UiaLock);
+ return found;
+}
+
+static long long cn1UiaNavigate(long long id, NavigateDirection direction) {
+ long long result = 0;
+ AcquireSRWLockShared(&cn1UiaLock);
+ if (id == 0) {
+ if (direction == NavigateDirection_FirstChild || direction == NavigateDirection_LastChild) {
+ for (size_t i = 0; i < cn1UiaNodes.size(); ++i) {
+ size_t index = direction == NavigateDirection_FirstChild ? i : cn1UiaNodes.size() - 1 - i;
+ if (cn1UiaNodes[index].parent < 0) { result = cn1UiaNodes[index].id; break; }
+ }
+ }
+ } else {
+ size_t current = cn1UiaNodes.size();
+ for (size_t i = 0; i < cn1UiaNodes.size(); ++i) if (cn1UiaNodes[i].id == id) { current = i; break; }
+ if (current < cn1UiaNodes.size()) {
+ const CN1UiaNode& node = cn1UiaNodes[current];
+ if (direction == NavigateDirection_Parent) result = node.parent < 0 ? 0 : node.parent;
+ if (direction == NavigateDirection_FirstChild || direction == NavigateDirection_LastChild) {
+ for (size_t i = 0; i < cn1UiaNodes.size(); ++i) {
+ size_t index = direction == NavigateDirection_FirstChild ? i : cn1UiaNodes.size() - 1 - i;
+ if (cn1UiaNodes[index].parent == id) { result = cn1UiaNodes[index].id; break; }
+ }
+ }
+ if (direction == NavigateDirection_NextSibling) {
+ for (size_t i = current + 1; i < cn1UiaNodes.size(); ++i)
+ if (cn1UiaNodes[i].parent == node.parent) { result = cn1UiaNodes[i].id; break; }
+ }
+ if (direction == NavigateDirection_PreviousSibling && current > 0) {
+ for (size_t i = current; i-- > 0; )
+ if (cn1UiaNodes[i].parent == node.parent) { result = cn1UiaNodes[i].id; break; }
+ }
+ }
+ }
+ ReleaseSRWLockShared(&cn1UiaLock);
+ return result;
+}
+
+static CONTROLTYPEID cn1UiaControlType(const std::wstring& role) {
+ if (role == L"BUTTON" || role == L"TOGGLE_BUTTON") return UIA_ButtonControlTypeId;
+ if (role == L"CHECKBOX" || role == L"SWITCH") return UIA_CheckBoxControlTypeId;
+ if (role == L"RADIO_BUTTON") return UIA_RadioButtonControlTypeId;
+ if (role == L"TEXT_FIELD" || role == L"SEARCH_FIELD") return UIA_EditControlTypeId;
+ if (role == L"SLIDER") return UIA_SliderControlTypeId;
+ if (role == L"PROGRESS_BAR") return UIA_ProgressBarControlTypeId;
+ if (role == L"LIST") return UIA_ListControlTypeId;
+ if (role == L"LIST_ITEM") return UIA_ListItemControlTypeId;
+ if (role == L"GRID") return UIA_DataGridControlTypeId;
+ if (role == L"ROW" || role == L"CELL") return UIA_DataItemControlTypeId;
+ if (role == L"TAB_LIST") return UIA_TabControlTypeId;
+ if (role == L"TAB") return UIA_TabItemControlTypeId;
+ if (role == L"DIALOG" || role == L"ALERT") return UIA_WindowControlTypeId;
+ if (role == L"MENU") return UIA_MenuControlTypeId;
+ if (role == L"MENU_ITEM") return UIA_MenuItemControlTypeId;
+ if (role == L"TOOLBAR") return UIA_ToolBarControlTypeId;
+ if (role == L"SCROLL_BAR") return UIA_ScrollBarControlTypeId;
+ if (role == L"COMBO_BOX") return UIA_ComboBoxControlTypeId;
+ if (role == L"TREE") return UIA_TreeControlTypeId;
+ if (role == L"TREE_ITEM") return UIA_TreeItemControlTypeId;
+ if (role == L"IMAGE") return UIA_ImageControlTypeId;
+ if (role == L"LINK") return UIA_HyperlinkControlTypeId;
+ if (role == L"STATIC_TEXT" || role == L"HEADING") return UIA_TextControlTypeId;
+ if (role == L"SEPARATOR") return UIA_SeparatorControlTypeId;
+ return UIA_GroupControlTypeId;
+}
+
+class CN1UiaProvider : public IRawElementProviderSimple, public IRawElementProviderFragment,
+ public IRawElementProviderFragmentRoot, public IInvokeProvider {
+ LONG refs;
+ long long id;
+public:
+ explicit CN1UiaProvider(long long nodeId) : refs(1), id(nodeId) {}
+ HRESULT STDMETHODCALLTYPE QueryInterface(REFIID iid, void** object) {
+ if (!object) return E_INVALIDARG;
+ *object = NULL;
+ if (iid == __uuidof(IUnknown) || iid == __uuidof(IRawElementProviderSimple))
+ *object = static_cast(this);
+ else if (iid == __uuidof(IRawElementProviderFragment))
+ *object = static_cast(this);
+ else if (iid == __uuidof(IRawElementProviderFragmentRoot) && id == 0)
+ *object = static_cast(this);
+ else if (iid == __uuidof(IInvokeProvider))
+ *object = static_cast(this);
+ if (!*object) return E_NOINTERFACE;
+ AddRef(); return S_OK;
+ }
+ ULONG STDMETHODCALLTYPE AddRef() { return (ULONG) InterlockedIncrement(&refs); }
+ ULONG STDMETHODCALLTYPE Release() { ULONG value = (ULONG) InterlockedDecrement(&refs); if (!value) delete this; return value; }
+
+ HRESULT STDMETHODCALLTYPE get_ProviderOptions(ProviderOptions* options) {
+ if (!options) return E_INVALIDARG; *options = ProviderOptions_ServerSideProvider; return S_OK;
+ }
+ HRESULT STDMETHODCALLTYPE GetPatternProvider(PATTERNID pattern, IUnknown** provider) {
+ if (!provider) return E_INVALIDARG; *provider = NULL;
+ CN1UiaNode node;
+ if (pattern == UIA_InvokePatternId && id != 0 && cn1UiaGet(id, node) && !node.actions.empty()) {
+ *provider = static_cast(this); AddRef();
+ }
+ return S_OK;
+ }
+ HRESULT STDMETHODCALLTYPE GetPropertyValue(PROPERTYID property, VARIANT* value) {
+ if (!value) return E_INVALIDARG;
+ VariantInit(value);
+ if (id == 0) {
+ if (property == UIA_NamePropertyId) { value->vt = VT_BSTR; value->bstrVal = SysAllocString(L"Codename One"); }
+ else if (property == UIA_ControlTypePropertyId) { value->vt = VT_I4; value->lVal = UIA_WindowControlTypeId; }
+ else if (property == UIA_IsControlElementPropertyId || property == UIA_IsContentElementPropertyId) { value->vt = VT_BOOL; value->boolVal = VARIANT_TRUE; }
+ return S_OK;
+ }
+ CN1UiaNode node; if (!cn1UiaGet(id, node)) return UIA_E_ELEMENTNOTAVAILABLE;
+ if (property == UIA_NamePropertyId) { value->vt = VT_BSTR; value->bstrVal = SysAllocString(node.label.c_str()); }
+ else if (property == UIA_HelpTextPropertyId) { value->vt = VT_BSTR; value->bstrVal = SysAllocString(node.description.c_str()); }
+ else if (property == UIA_ValueValuePropertyId) { value->vt = VT_BSTR; value->bstrVal = SysAllocString(node.value.c_str()); }
+ else if (property == UIA_ControlTypePropertyId) { value->vt = VT_I4; value->lVal = cn1UiaControlType(node.role); }
+ else if (property == UIA_LocalizedControlTypePropertyId) { value->vt = VT_BSTR; value->bstrVal = SysAllocString(node.role.c_str()); }
+ else if (property == UIA_IsEnabledPropertyId) { value->vt = VT_BOOL; value->boolVal = (node.flags & 4) ? VARIANT_TRUE : VARIANT_FALSE; }
+ else if (property == UIA_HasKeyboardFocusPropertyId) { value->vt = VT_BOOL; value->boolVal = (node.flags & 2) ? VARIANT_TRUE : VARIANT_FALSE; }
+ else if (property == UIA_IsKeyboardFocusablePropertyId) { value->vt = VT_BOOL; value->boolVal = (node.flags & 1) ? VARIANT_TRUE : VARIANT_FALSE; }
+ else if (property == UIA_IsControlElementPropertyId || property == UIA_IsContentElementPropertyId) { value->vt = VT_BOOL; value->boolVal = VARIANT_TRUE; }
+ return S_OK;
+ }
+ HRESULT STDMETHODCALLTYPE get_HostRawElementProvider(IRawElementProviderSimple** provider) {
+ if (!provider) return E_INVALIDARG; *provider = NULL;
+ return id == 0 && cn1Win.hwnd ? UiaHostProviderFromHwnd(cn1Win.hwnd, provider) : S_OK;
+ }
+ HRESULT STDMETHODCALLTYPE Navigate(NavigateDirection direction, IRawElementProviderFragment** result) {
+ if (!result) return E_INVALIDARG; *result = NULL;
+ long long target = cn1UiaNavigate(id, direction);
+ if (target || (direction == NavigateDirection_Parent && id != 0)) *result = new CN1UiaProvider(target);
+ return S_OK;
+ }
+ HRESULT STDMETHODCALLTYPE GetRuntimeId(SAFEARRAY** runtimeId) {
+ if (!runtimeId) return E_INVALIDARG; *runtimeId = NULL;
+ if (id == 0) return S_OK;
+ SAFEARRAY* array = SafeArrayCreateVector(VT_I4, 0, 3); if (!array) return E_OUTOFMEMORY;
+ LONG indexes[] = {0, 1, 2}; int values[] = {UiaAppendRuntimeId, (int)(id >> 32), (int)id};
+ for (int i = 0; i < 3; ++i) SafeArrayPutElement(array, &indexes[i], &values[i]);
+ *runtimeId = array; return S_OK;
+ }
+ HRESULT STDMETHODCALLTYPE get_BoundingRectangle(UiaRect* rectangle) {
+ if (!rectangle) return E_INVALIDARG;
+ if (id == 0) { RECT r; GetWindowRect(cn1Win.hwnd, &r); rectangle->left=r.left; rectangle->top=r.top; rectangle->width=r.right-r.left; rectangle->height=r.bottom-r.top; return S_OK; }
+ CN1UiaNode node; if (!cn1UiaGet(id, node)) return UIA_E_ELEMENTNOTAVAILABLE;
+ POINT point = {node.x, node.y}; ClientToScreen(cn1Win.hwnd, &point);
+ rectangle->left=point.x; rectangle->top=point.y; rectangle->width=node.width; rectangle->height=node.height; return S_OK;
+ }
+ HRESULT STDMETHODCALLTYPE GetEmbeddedFragmentRoots(SAFEARRAY** roots) { if (!roots) return E_INVALIDARG; *roots=NULL; return S_OK; }
+ HRESULT STDMETHODCALLTYPE SetFocus() { return S_OK; }
+ HRESULT STDMETHODCALLTYPE get_FragmentRoot(IRawElementProviderFragmentRoot** root) {
+ if (!root) return E_INVALIDARG; *root = new CN1UiaProvider(0); return S_OK;
+ }
+ HRESULT STDMETHODCALLTYPE ElementProviderFromPoint(double x, double y, IRawElementProviderFragment** result) {
+ if (!result) return E_INVALIDARG; *result=NULL; POINT p={(LONG)x,(LONG)y}; ScreenToClient(cn1Win.hwnd,&p);
+ long long found=0; AcquireSRWLockShared(&cn1UiaLock);
+ for (size_t i=0;i=n.x&&p.y>=n.y&&p.xRelease(); return result;
+}
+
+extern "C" {
+JAVA_VOID com_codename1_impl_windows_WindowsNative_accessibilityBegin__(CODENAME_ONE_THREAD_STATE) {
+ AcquireSRWLockExclusive(&cn1UiaLock); cn1UiaPending.clear(); ReleaseSRWLockExclusive(&cn1UiaLock);
+}
+JAVA_VOID com_codename1_impl_windows_WindowsNative_accessibilityNode___long_long_java_lang_String_java_lang_String_java_lang_String_java_lang_String_int_int_int_int_int(
+ CODENAME_ONE_THREAD_STATE, JAVA_LONG id, JAVA_LONG parent, JAVA_OBJECT role, JAVA_OBJECT label,
+ JAVA_OBJECT description, JAVA_OBJECT value, JAVA_INT x, JAVA_INT y, JAVA_INT width, JAVA_INT height, JAVA_INT flags) {
+ extern const char* stringToUTF8(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT);
+ CN1UiaNode node; node.id=id; node.parent=parent; node.actionTarget=id; node.x=x; node.y=y; node.width=width; node.height=height; node.flags=flags;
+ node.role=role==JAVA_NULL?L"":cn1UiaWide(stringToUTF8(threadStateData,role));
+ node.label=label==JAVA_NULL?L"":cn1UiaWide(stringToUTF8(threadStateData,label));
+ node.description=description==JAVA_NULL?L"":cn1UiaWide(stringToUTF8(threadStateData,description));
+ node.value=value==JAVA_NULL?L"":cn1UiaWide(stringToUTF8(threadStateData,value));
+ AcquireSRWLockExclusive(&cn1UiaLock); cn1UiaPending.push_back(node); ReleaseSRWLockExclusive(&cn1UiaLock);
+}
+JAVA_VOID com_codename1_impl_windows_WindowsNative_accessibilityAction___long_java_lang_String_int_java_lang_String(
+ CODENAME_ONE_THREAD_STATE, JAVA_LONG nodeId, JAVA_OBJECT actionId, JAVA_INT actionHash, JAVA_OBJECT label) {
+ extern const char* stringToUTF8(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT);
+ std::wstring idText=actionId==JAVA_NULL?L"":cn1UiaWide(stringToUTF8(threadStateData,actionId));
+ std::wstring text=label==JAVA_NULL?L"":cn1UiaWide(stringToUTF8(threadStateData,label));
+ AcquireSRWLockExclusive(&cn1UiaLock);
+ for(size_t i=0;iRelease();
+}
+}
+
+#endif
diff --git a/Ports/WindowsPort/nativeSources/cn1_windows_window.cpp b/Ports/WindowsPort/nativeSources/cn1_windows_window.cpp
index a474397b938..94dd27557e2 100644
--- a/Ports/WindowsPort/nativeSources/cn1_windows_window.cpp
+++ b/Ports/WindowsPort/nativeSources/cn1_windows_window.cpp
@@ -471,6 +471,8 @@ static int cn1WinHandleGesture(HWND hwnd, LPARAM lParam) {
LRESULT CALLBACK cn1WinWndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
switch (msg) {
+ case WM_GETOBJECT:
+ return cn1WinAccessibilityObject(hwnd, wParam, lParam);
case WM_LBUTTONDOWN:
cn1WinButtonDown(hwnd, CN1_PE_MASK_PRIMARY);
cn1WinPushEvent(CN1_EVENT_POINTER_PRESSED, GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam),
diff --git a/Ports/WindowsPort/src/com/codename1/impl/windows/WindowsImplementation.java b/Ports/WindowsPort/src/com/codename1/impl/windows/WindowsImplementation.java
index 11e73024e38..763515be459 100644
--- a/Ports/WindowsPort/src/com/codename1/impl/windows/WindowsImplementation.java
+++ b/Ports/WindowsPort/src/com/codename1/impl/windows/WindowsImplementation.java
@@ -35,6 +35,10 @@
import com.codename1.ui.Stroke;
import com.codename1.ui.geom.PathIterator;
import com.codename1.ui.geom.Shape;
+import com.codename1.ui.accessibility.AccessibilityAction;
+import com.codename1.ui.accessibility.AccessibilityManager;
+import com.codename1.ui.accessibility.AccessibilityNodeSnapshot;
+import com.codename1.ui.accessibility.AccessibilityTreeSnapshot;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ByteArrayInputStream;
@@ -71,6 +75,7 @@ public class WindowsImplementation extends CodenameOneImplementation {
private static final int EVENT_MOUSE_HWHEEL = 9;
private static final int EVENT_PINCH = 10;
private static final int EVENT_ROTATE = 11;
+ private static final int EVENT_ACCESSIBILITY_ACTION = 12;
// The native gesture events encode their float (incremental scale / radians) as
// an int in 1/10000 units; see CN1_GESTURE_FIXED in cn1_windows.h.
@@ -680,6 +685,9 @@ private void drainInput() {
case EVENT_CLOSE:
Display.getInstance().exitApplication();
break;
+ case EVENT_ACCESSIBILITY_ACTION:
+ AccessibilityManager.getInstance().performActionByHash(x & 0xffffffffL, key, null);
+ break;
default:
break;
}
@@ -692,6 +700,49 @@ private void drainInput() {
}
}
+ @Override
+ public void accessibilityTreeChanged(int changeType) {
+ AccessibilityTreeSnapshot tree = getAccessibilityTreeSnapshot();
+ WindowsNative.accessibilityBegin();
+ for (AccessibilityNodeSnapshot node : tree.getNodes().values()) {
+ int flags = (node.isFocusable() ? 1 : 0) | (node.isFocused() ? 2 : 0)
+ | (Boolean.TRUE.equals(node.getEnabled()) ? 4 : 0)
+ | (Boolean.TRUE.equals(node.getSelected()) ? 8 : 0)
+ | (node.getChecked().ordinal() << 4)
+ | (Boolean.TRUE.equals(node.getExpanded()) ? 64 : 0)
+ | (Boolean.TRUE.equals(node.getInvalid()) ? 128 : 0);
+ com.codename1.ui.geom.Rectangle b = node.getBounds();
+ WindowsNative.accessibilityNode(node.getId(), node.getParentId(), node.getRole().name(), node.getLabel(),
+ accessibilityDescription(node), node.getValue(), b.getX(), b.getY(), b.getWidth(), b.getHeight(), flags);
+ for (AccessibilityAction action : node.getActions()) {
+ if (action.isEnabled()) {
+ WindowsNative.accessibilityAction(node.getId(), action.getId(), action.getId().hashCode(),
+ action.getLabel() == null ? action.getId() : action.getLabel());
+ }
+ }
+ }
+ WindowsNative.accessibilityEnd(changeType);
+ }
+
+ private String accessibilityDescription(AccessibilityNodeSnapshot node) {
+ StringBuilder out = new StringBuilder();
+ if (node.getHint() != null) out.append(node.getHint());
+ if (node.getDescription() != null) {
+ if (out.length() > 0) out.append(". ");
+ out.append(node.getDescription());
+ }
+ if (node.getValidationError() != null) {
+ if (out.length() > 0) out.append(". ");
+ out.append(node.getValidationError());
+ }
+ return out.length() == 0 ? null : out.toString();
+ }
+
+ @Override
+ public boolean isAccessibilityTreeSupported() {
+ return true;
+ }
+
/* --------------------------------------------------- local notifications
* Desktop semantic (mirrors the JavaSE port): while the app runs, a Timer
* fires the notification at its scheduled time and Shell_NotifyIcon shows a
diff --git a/Ports/WindowsPort/src/com/codename1/impl/windows/WindowsNative.java b/Ports/WindowsPort/src/com/codename1/impl/windows/WindowsNative.java
index c0bc766ff92..8d6d1d041e9 100644
--- a/Ports/WindowsPort/src/com/codename1/impl/windows/WindowsNative.java
+++ b/Ports/WindowsPort/src/com/codename1/impl/windows/WindowsNative.java
@@ -143,6 +143,13 @@ public static native void setTransform(long graphics, float m00, float m10, floa
*/
public static native boolean pollEvent(int[] out);
+ /** Rebuilds the Windows UI Automation virtual fragment tree. */
+ public static native void accessibilityBegin();
+ public static native void accessibilityNode(long id, long parentId, String role, String label,
+ String description, String value, int x, int y, int width, int height, int stateFlags);
+ public static native void accessibilityAction(long nodeId, String actionId, int actionHash, String label);
+ public static native void accessibilityEnd(int changeType);
+
/**
* True when an integrated or external touch digitizer is present, used for
* {@code Display.isTouchScreen()} (queries GetSystemMetrics(SM_DIGITIZER)).
diff --git a/Ports/iOSPort/nativeSources/IOSNative.m b/Ports/iOSPort/nativeSources/IOSNative.m
index 422bf34b4af..90a482a0573 100644
--- a/Ports/iOSPort/nativeSources/IOSNative.m
+++ b/Ports/iOSPort/nativeSources/IOSNative.m
@@ -13481,6 +13481,217 @@ void com_codename1_impl_ios_IOSNative_announceForAccessibility___java_lang_Strin
#endif // !TARGET_OS_WATCH
}
+#if !TARGET_OS_WATCH
+@interface CN1AccessibilityCustomAction : UIAccessibilityCustomAction
+@property(nonatomic, retain) NSString *cn1ActionId;
+@end
+
+@implementation CN1AccessibilityCustomAction
+@end
+
+@interface CN1AccessibilityElement : UIAccessibilityElement
+@property(nonatomic, assign) long long cn1NodeId;
+@property(nonatomic, retain) NSArray *cn1Actions;
+@end
+
+static NSString *CN1AccessibilityActionId(NSArray *actions, NSString *wanted) {
+ for (NSDictionary *action in actions) {
+ if ([wanted isEqualToString:[action objectForKey:@"id"]]
+ && ![[action objectForKey:@"enabled"] isEqual:@NO]) {
+ return wanted;
+ }
+ }
+ return nil;
+}
+
+static void CN1PerformAccessibilityAction(long long nodeId, NSString *actionId, NSString *argument) {
+ if (actionId == nil) return;
+ JAVA_OBJECT javaAction = fromNSString(CN1_THREAD_GET_STATE_PASS_ARG actionId);
+ JAVA_OBJECT javaArgument = argument == nil ? JAVA_NULL : fromNSString(CN1_THREAD_GET_STATE_PASS_ARG argument);
+ com_codename1_impl_ios_IOSImplementation_performAccessibilityActionFromNative___long_java_lang_String_java_lang_String(
+ CN1_THREAD_GET_STATE_PASS_ARG (JAVA_LONG)nodeId, javaAction, javaArgument);
+}
+
+@implementation CN1AccessibilityElement
+- (BOOL)accessibilityActivate {
+ NSString *action = CN1AccessibilityActionId(self.cn1Actions, @"activate");
+ if (action == nil) action = CN1AccessibilityActionId(self.cn1Actions, @"focus");
+ if (action == nil) return NO;
+ CN1PerformAccessibilityAction(self.cn1NodeId, action, nil);
+ return YES;
+}
+
+- (void)accessibilityIncrement {
+ CN1PerformAccessibilityAction(self.cn1NodeId,
+ CN1AccessibilityActionId(self.cn1Actions, @"increment"), nil);
+}
+
+- (void)accessibilityDecrement {
+ CN1PerformAccessibilityAction(self.cn1NodeId,
+ CN1AccessibilityActionId(self.cn1Actions, @"decrement"), nil);
+}
+
+- (BOOL)accessibilityPerformEscape {
+ NSString *action = CN1AccessibilityActionId(self.cn1Actions, @"dismiss");
+ if (action == nil) return NO;
+ CN1PerformAccessibilityAction(self.cn1NodeId, action, nil);
+ return YES;
+}
+
+- (BOOL)accessibilityScroll:(UIAccessibilityScrollDirection)direction {
+ NSString *action = nil;
+ if (direction == UIAccessibilityScrollDirectionDown || direction == UIAccessibilityScrollDirectionRight
+ || direction == UIAccessibilityScrollDirectionNext) {
+ action = CN1AccessibilityActionId(self.cn1Actions, @"scrollForward");
+ } else {
+ action = CN1AccessibilityActionId(self.cn1Actions, @"scrollBackward");
+ }
+ if (action == nil) return NO;
+ CN1PerformAccessibilityAction(self.cn1NodeId, action, nil);
+ return YES;
+}
+
+- (BOOL)cn1PerformCustomAction:(CN1AccessibilityCustomAction *)action {
+ if (action.cn1ActionId == nil) return NO;
+ CN1PerformAccessibilityAction(self.cn1NodeId, action.cn1ActionId, nil);
+ return YES;
+}
+@end
+
+static NSMutableDictionary *cn1AccessibilityLiveValues;
+
+static id CN1JSONValue(NSDictionary *node, NSString *key) {
+ id value = [node objectForKey:key];
+ return value == [NSNull null] ? nil : value;
+}
+
+static UIAccessibilityTraits CN1AccessibilityTraitsForNode(NSDictionary *node) {
+ NSString *role = CN1JSONValue(node, @"role");
+ UIAccessibilityTraits traits = UIAccessibilityTraitNone;
+ if ([role isEqualToString:@"BUTTON"] || [role isEqualToString:@"TOGGLE_BUTTON"]
+ || [role isEqualToString:@"CHECKBOX"] || [role isEqualToString:@"RADIO_BUTTON"]
+ || [role isEqualToString:@"SWITCH"] || [role isEqualToString:@"TAB"]
+ || [role isEqualToString:@"MENU_ITEM"]) traits |= UIAccessibilityTraitButton;
+ if ([role isEqualToString:@"LINK"]) traits |= UIAccessibilityTraitLink;
+ if ([role isEqualToString:@"IMAGE"]) traits |= UIAccessibilityTraitImage;
+ if ([role isEqualToString:@"STATIC_TEXT"]) traits |= UIAccessibilityTraitStaticText;
+ if ([role isEqualToString:@"SEARCH_FIELD"]) traits |= UIAccessibilityTraitSearchField;
+ if ([role isEqualToString:@"HEADING"] || [CN1JSONValue(node, @"headingLevel") intValue] > 0)
+ traits |= UIAccessibilityTraitHeader;
+ if ([role isEqualToString:@"SLIDER"] || [role isEqualToString:@"SPIN_BUTTON"])
+ traits |= UIAccessibilityTraitAdjustable;
+ if ([CN1JSONValue(node, @"selected") boolValue]) traits |= UIAccessibilityTraitSelected;
+ id enabled = CN1JSONValue(node, @"enabled");
+ if (enabled != nil && ![enabled boolValue]) traits |= UIAccessibilityTraitNotEnabled;
+ if (![[CN1JSONValue(node, @"liveRegion") description] isEqualToString:@"OFF"])
+ traits |= UIAccessibilityTraitUpdatesFrequently;
+ return traits;
+}
+
+static NSString *CN1AccessibilityValueForNode(NSDictionary *node) {
+ NSString *value = CN1JSONValue(node, @"value");
+ NSDictionary *range = CN1JSONValue(node, @"range");
+ if (value == nil && range != nil) {
+ value = CN1JSONValue(range, @"text");
+ if (value == nil) value = [[range objectForKey:@"current"] stringValue];
+ }
+ NSString *checked = CN1JSONValue(node, @"checked");
+ if ([checked isEqualToString:@"CHECKED"]) value = value == nil ? @"Checked" : [value stringByAppendingString:@", Checked"];
+ else if ([checked isEqualToString:@"UNCHECKED"]) value = value == nil ? @"Unchecked" : [value stringByAppendingString:@", Unchecked"];
+ else if ([checked isEqualToString:@"MIXED"]) value = value == nil ? @"Mixed" : [value stringByAppendingString:@", Mixed"];
+ id expanded = CN1JSONValue(node, @"expanded");
+ if (expanded != nil) {
+ NSString *state = [expanded boolValue] ? @"Expanded" : @"Collapsed";
+ value = value == nil ? state : [value stringByAppendingFormat:@", %@", state];
+ }
+ id invalid = CN1JSONValue(node, @"invalid");
+ if (invalid != nil && [invalid boolValue]) value = value == nil ? @"Invalid" : [value stringByAppendingString:@", Invalid"];
+ return value;
+}
+
+void com_codename1_impl_ios_IOSNative_updateAccessibilityTree___java_lang_String_int(
+ CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT json, JAVA_INT changeType) {
+ if (json == JAVA_NULL) return;
+ POOL_BEGIN();
+ NSString *jsonString = toNSString(CN1_THREAD_STATE_PASS_ARG json);
+ NSData *data = [jsonString dataUsingEncoding:NSUTF8StringEncoding];
+ NSDictionary *tree = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
+ NSArray *nodes = [tree objectForKey:@"nodes"];
+ dispatch_async(dispatch_get_main_queue(), ^{
+ UIView *container = (UIView *)[[CodenameOne_GLViewController instance] eaglView];
+ if (container == nil) return;
+ NSMutableArray *elements = [NSMutableArray arrayWithCapacity:[nodes count]];
+ NSMutableDictionary *elementsById = [NSMutableDictionary dictionary];
+ if (cn1AccessibilityLiveValues == nil) cn1AccessibilityLiveValues = [[NSMutableDictionary alloc] init];
+ for (NSDictionary *node in nodes) {
+ CN1AccessibilityElement *element = [[CN1AccessibilityElement alloc] initWithAccessibilityContainer:container];
+ NSNumber *nodeId = [node objectForKey:@"id"];
+ element.cn1NodeId = [nodeId longLongValue];
+ element.cn1Actions = CN1JSONValue(node, @"actions");
+ element.accessibilityLabel = CN1JSONValue(node, @"label");
+ NSString *hint = CN1JSONValue(node, @"hint");
+ NSString *error = CN1JSONValue(node, @"error");
+ element.accessibilityHint = error == nil ? hint : hint == nil ? error : [NSString stringWithFormat:@"%@. %@", hint, error];
+ element.accessibilityValue = CN1AccessibilityValueForNode(node);
+ element.accessibilityIdentifier = CN1JSONValue(node, @"identifier");
+ element.accessibilityTraits = CN1AccessibilityTraitsForNode(node);
+ element.accessibilityViewIsModal = [CN1JSONValue(node, @"modal") boolValue];
+ NSArray *bounds = [node objectForKey:@"bounds"];
+ if ([bounds count] == 4) {
+ CGFloat scale = scaleValue <= 0 ? [UIScreen mainScreen].scale : scaleValue;
+ element.accessibilityFrameInContainerSpace = CGRectMake(
+ [[bounds objectAtIndex:0] doubleValue] / scale,
+ [[bounds objectAtIndex:1] doubleValue] / scale,
+ [[bounds objectAtIndex:2] doubleValue] / scale,
+ [[bounds objectAtIndex:3] doubleValue] / scale);
+ }
+ NSMutableArray *custom = [NSMutableArray array];
+ for (NSDictionary *action in element.cn1Actions) {
+ NSString *actionId = [action objectForKey:@"id"];
+ if ([actionId isEqualToString:@"activate"] || [actionId isEqualToString:@"focus"]
+ || [actionId isEqualToString:@"increment"] || [actionId isEqualToString:@"decrement"]
+ || [actionId isEqualToString:@"dismiss"] || [actionId isEqualToString:@"scrollForward"]
+ || [actionId isEqualToString:@"scrollBackward"] || ![[action objectForKey:@"enabled"] boolValue]) continue;
+ NSString *name = CN1JSONValue(action, @"label");
+ if (name == nil) name = actionId;
+ CN1AccessibilityCustomAction *customAction = [[CN1AccessibilityCustomAction alloc]
+ initWithName:name target:element selector:@selector(cn1PerformCustomAction:)];
+ customAction.cn1ActionId = actionId;
+ [custom addObject:customAction];
+ }
+ element.accessibilityCustomActions = custom;
+ [elements addObject:element];
+ [elementsById setObject:element forKey:nodeId];
+
+ NSString *live = CN1JSONValue(node, @"liveRegion");
+ if (live != nil && ![live isEqualToString:@"OFF"]) {
+ NSString *newValue = [NSString stringWithFormat:@"%@|%@", element.accessibilityLabel ?: @"", element.accessibilityValue ?: @""];
+ NSString *oldValue = [cn1AccessibilityLiveValues objectForKey:nodeId];
+ if (oldValue != nil && ![oldValue isEqualToString:newValue]) {
+ UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification,
+ element.accessibilityLabel ?: element.accessibilityValue);
+ }
+ [cn1AccessibilityLiveValues setObject:newValue forKey:nodeId];
+ }
+ }
+ container.isAccessibilityElement = NO;
+ container.accessibilityElements = elements;
+ if ((changeType & 256) != 0) {
+ UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification,
+ [elements count] == 0 ? nil : [elements objectAtIndex:0]);
+ } else {
+ UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, nil);
+ }
+ });
+ POOL_END();
+}
+#else
+void com_codename1_impl_ios_IOSNative_updateAccessibilityTree___java_lang_String_int(
+ CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT json, JAVA_INT changeType) {
+ // watchOS uses a separate SwiftUI accessibility model.
+}
+#endif
+
// ====================================================================
// Crypto bridge -- implementations of the native methods on IOSNative
// that back com.codename1.security.{Cipher,Signature,SecureRandom,
diff --git a/Ports/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java b/Ports/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java
index da7451cbee4..5f1454230bd 100644
--- a/Ports/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java
+++ b/Ports/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java
@@ -11671,6 +11671,22 @@ public void announceForAccessibility(final Component cmp, final String text) {
IOSNative.announceForAccessibility(text);
}
+ @Override
+ public void accessibilityTreeChanged(int changeType) {
+ IOSNative.updateAccessibilityTree(getAccessibilityTreeSnapshot().toJson(), changeType);
+ }
+
+ @Override
+ public boolean isAccessibilityTreeSupported() {
+ return true;
+ }
+
+ public static void performAccessibilityActionFromNative(long nodeId, String actionId, String argument) {
+ if (instance != null) {
+ instance.performAccessibilityAction(nodeId, actionId, argument);
+ }
+ }
+
// ================================================================
// Crypto bridge -- routes through CN1Crypto.{h,m} in nativeSources/
// (the corresponding native methods live on IOSNative). The defaults
diff --git a/Ports/iOSPort/src/com/codename1/impl/ios/IOSNative.java b/Ports/iOSPort/src/com/codename1/impl/ios/IOSNative.java
index 58a714251cc..c935634bd14 100644
--- a/Ports/iOSPort/src/com/codename1/impl/ios/IOSNative.java
+++ b/Ports/iOSPort/src/com/codename1/impl/ios/IOSNative.java
@@ -1213,6 +1213,8 @@ native void startTagRead(int requestId, String alertMessage,
public static native void announceForAccessibility(String text);
+ public static native void updateAccessibilityTree(String json, int changeType);
+
// ============================================================
// Crypto bridge -- backed by CN1Crypto.{h,m} in nativeSources/.
//
diff --git a/docs/developer-guide/Accessibility-Semantics.asciidoc b/docs/developer-guide/Accessibility-Semantics.asciidoc
new file mode 100644
index 00000000000..6cd09e7f876
--- /dev/null
+++ b/docs/developer-guide/Accessibility-Semantics.asciidoc
@@ -0,0 +1,218 @@
+= Accessibility Semantics
+
+Codename One components are lightweight: they are painted by Codename One instead of being represented by a native widget on every platform. The accessibility semantics API builds an explicit, immutable virtual tree from those components and presents it to VoiceOver, TalkBack, Windows UI Automation, AT-SPI, Java Access Bridge, and web accessibility APIs.
+
+The semantic tree is separate from the visual tree. This is important for renderer-backed controls, merged cards, decorative content, custom controls, and applications that need a reading order different from paint order.
+
+== Built-in semantics
+
+Standard components work without extra configuration. `Button`, `CheckBox`, `RadioButton`, `Slider`, text fields, lists, tables, tabs, labels, dialogs, and containers infer appropriate roles, values, states, collection metadata, and standard actions. Renderer-backed `List` rows are exposed as stable virtual children even though no `Component` exists for each painted row.
+
+`Component.setAccessibilityText()` remains supported. It is a compatibility alias for the semantic label. New code should use `getSemantics()` when it needs more than a label.
+
+[source,java]
+----
+Button save = new Button("Save");
+save.getSemantics()
+ .setHint("Saves the edited profile")
+ .setIdentifier("profile-save");
+----
+
+Changes to text, selection, focus, enabled state, bounds, scrolling, ranges, tabs, and the current `Form` automatically invalidate the native semantic tree. Custom components should call `accessibilityChanged()` after a semantic value changes outside a standard setter.
+
+== Roles, names, values, and state
+
+`AccessibilityRole` contains portable control roles including buttons, toggle buttons, switches, checkboxes, radio buttons, headings, links, images, text and search fields, sliders, progress indicators, lists, grids, rows, cells, headers, tabs, dialogs, alerts, menus, toolbars, combo boxes, trees, and separators. A port maps an unsupported role to the closest native role without discarding its label or state.
+
+[source,java]
+----
+Component wifiSwitch = new MyPaintedSwitch();
+wifiSwitch.getSemantics()
+ .setRole(AccessibilityRole.SWITCH)
+ .setLabel("Wi-Fi")
+ .setChecked(AccessibilityCheckedState.CHECKED)
+ .setEnabled(Boolean.TRUE)
+ .setHint("Double tap to turn Wi-Fi off");
+
+Label title = new Label("Network");
+title.getSemantics()
+ .setHeadingLevel(2)
+ .setIdentifier("network-heading");
+----
+
+Tri-state controls use `UNCHECKED`, `CHECKED`, or `MIXED`. Nullable Boolean properties distinguish an explicit state from an unspecified state. Available properties include selected, expanded, enabled, invalid, busy, read-only, required, multiline, obscured, pressed, and current. `setValidationError()` supplies both the error message and invalid state. `setPaneTitle()` describes a newly displayed pane or screen, and `setModal(true)` identifies a modal scope.
+
+Use `setRoleDescription()` only when the platform role is not sufficiently specific. Localize labels, hints, descriptions, errors, pane titles, role descriptions, and custom action labels.
+
+== Ranges and editable values
+
+`AccessibilityRange` describes minimum, maximum, current value, increment, and an optional spoken value. The spoken value is useful for ranges such as ratings and durations where a raw number is ambiguous.
+
+[source,java]
+----
+rating.getSemantics()
+ .setRole(AccessibilityRole.SLIDER)
+ .setLabel("Rating")
+ .setRange(new AccessibilityRange(0, 5, 3, 1, "3 out of 5"));
+----
+
+Editable controls can provide `SET_VALUE` or `SET_TEXT` actions. Sliders and other adjustable controls should expose `INCREMENT` and `DECREMENT`. Standard components infer these actions.
+
+== Custom accessibility actions
+
+An `AccessibilityAction` has a stable ID, a localized label, an enabled state, and a handler. Handlers always execute on the Codename One EDT, including when the action originates on a native accessibility thread.
+
+[source,java]
+----
+card.getSemantics().addAction(new AccessibilityAction(
+ "archive",
+ "Archive message",
+ new AccessibilityAction.Handler() {
+ public boolean perform(Component component, Object argument) {
+ archiveMessage();
+ return true;
+ }
+ }
+));
+----
+
+Use the standard IDs in `AccessibilityAction` for activation, long press, increment, decrement, set value, set text, selection, character or word cursor movement, focus, show-on-screen, dismiss, expand, collapse, scrolling, and clipboard operations. A custom ID must be stable and its label must not be null.
+
+== Grouping and virtual descendants
+
+`AccessibilityGrouping` controls how a component participates in the semantic tree:
+
+* `AUTO` uses normal component inference.
+* `LEAF` exposes the component but none of its descendants.
+* `GROUP` exposes the component as a semantic container and retains its descendants.
+* `MERGE_DESCENDANTS` combines descendant labels, descriptions, values, state, and actions into one node.
+* `EXCLUDE` removes the component but promotes its descendants to the semantic parent.
+* `EXCLUDE_SUBTREE` removes the component and all descendants.
+
+[source,java]
+----
+summaryCard.getSemantics()
+ .setRole(AccessibilityRole.GENERIC)
+ .setGrouping(AccessibilityGrouping.MERGE_DESCENDANTS);
+----
+
+For content that is painted without child components, attach an `AccessibilityChildProvider`, or add standalone `AccessibilityNode` children. Every virtual node must use a stable `virtualKey`. Stable keys preserve native focus while rows are updated or recycled.
+
+[source,java]
+----
+chart.getSemantics().setChildProvider(new AccessibilityChildProvider() {
+ public List getAccessibilityChildren(Component owner) {
+ List result = new ArrayList();
+ for (Point point : model.getPoints()) {
+ AccessibilityNode node = new AccessibilityNode("point-" + point.getId());
+ node.setRole(AccessibilityRole.IMAGE)
+ .setLabel(point.getLabel())
+ .setValue(point.getFormattedValue())
+ .setBounds(point.getBounds());
+ result.add(node);
+ }
+ return result;
+ }
+});
+----
+
+Virtual bounds are relative to the owning component. Component nodes use their clipped absolute bounds.
+
+== Traversal order
+
+Visual order is the default reading order. `setSortKey()` assigns an ordered semantic key among siblings. `setTraversalBefore()` and `setTraversalAfter()` express a direct relationship when a numeric order would be fragile.
+
+[source,java]
+----
+cancel.getSemantics().setSortKey(1);
+continueButton.getSemantics().setSortKey(2);
+help.getSemantics().setTraversalAfter(continueButton);
+----
+
+Keep traversal relationships within the same semantic parent. The tree builder resolves these constraints deterministically and preserves stable node IDs.
+
+== Live regions and screen changes
+
+Set `AccessibilityLiveRegion.POLITE` for non-urgent updates and `ASSERTIVE` for errors or urgent status. `OFF` is the default. Live changes produce the corresponding platform notification or ARIA live update.
+
+[source,java]
+----
+status.getSemantics()
+ .setRole(AccessibilityRole.ALERT)
+ .setLiveRegion(AccessibilityLiveRegion.POLITE);
+status.setText("Upload complete");
+----
+
+Form changes and nodes with a pane title produce screen or pane transition notifications. `Display.announceForAccessibility()` remains available for an exceptional announcement that is not represented by persistent UI. Prefer a live region for state that is visible on screen.
+
+== Collections
+
+`AccessibilityCollectionInfo` describes row and column counts, hierarchy, and selection mode. `AccessibilityCollectionItemInfo` describes row and column index, spans, position and size in a set, nesting level, and header status.
+
+[source,java]
+----
+grid.getSemantics().setCollectionInfo(
+ new AccessibilityCollectionInfo(20, 3, false,
+ AccessibilityCollectionInfo.SELECTION_MULTIPLE));
+
+cell.getSemantics().setCollectionItemInfo(
+ new AccessibilityCollectionItemInfo(
+ 4, 1, 2, 1, 5, 20, 1, false));
+----
+
+Row and column indexes are zero-based. Position and set size are one-based; use `-1` when unknown. Lists, tables, tabs, and their items infer this metadata where possible.
+
+== Inspecting and testing semantics
+
+`AccessibilityInspector` returns an immutable snapshot. A snapshot is safe to inspect without racing native accessibility callbacks and includes stable IDs, hierarchy, bounds, resolved state, actions, range and collection data. `toJson()` is suitable for diagnostic reports and tooling.
+
+[source,java]
+----
+AccessibilityTreeSnapshot tree =
+ AccessibilityInspector.snapshot(myForm);
+
+AccessibilityAssertions.assertNoErrors(tree);
+AccessibilityAssertions.assertNoUnlabeledInteractiveNodes(tree);
+
+String diagnosticJson = tree.toJson();
+AccessibilityNodeSnapshot hit = tree.getNodeAt(screenX, screenY);
+----
+
+`AccessibilityAssertions.audit()` reports machine-readable `AccessibilityIssue` values for unlabeled controls, invalid ranges, duplicate identifiers or actions, invalid collection positions, missing parents, cycles, unnamed dialogs, and unlabeled custom actions. Warnings also identify suspicious role/state combinations and empty bounds.
+
+The Java SE Component Inspector provides *Copy Accessibility Tree JSON* and *Audit Accessibility Tree* actions. Tests should assert resolved semantic properties instead of platform-specific spoken sentences. The `scripts/hellocodenameone` suite contains the cross-port conformance fixture used by native CI.
+
+== Platform mappings
+
+[cols="1,2,2"]
+|===
+|Target |Native representation |Notable behavior
+
+|iOS, iPadOS, and Mac Catalyst
+|`UIAccessibilityElement` instances owned by the rendering view
+|Traits, values, frames, custom actions, adjustable actions, live announcements, and screen/layout transitions
+
+|Android
+|`AccessibilityNodeProvider` virtual descendants
+|Class/role mapping, checked and mixed state, headings, pane titles, errors, ranges, collections, focus, hit testing, and custom actions
+
+|Windows native
+|UI Automation fragment-root provider
+|Virtual fragments, control types, names, values, state, bounds, navigation, focus lookup, hit testing, and action dispatch
+
+|Linux native
+|Transparent GTK semantic widgets exposed through ATK/AT-SPI
+|Roles, names, descriptions, state, hierarchy, bounds, and action dispatch
+
+|Java SE simulator and desktop
+|Swing `AccessibleContext` virtual children
+|Roles, states, values, geometry, accessible actions, and Component Inspector integration
+
+|JavaScript
+|A synchronized off-screen DOM tree
+|ARIA roles, names, state, ranges, live regions, collection metadata, traversal order, and action routing; the painted canvas is hidden from the accessibility tree
+|===
+
+The API intentionally goes beyond a label-only model: it includes stable virtual children, explicit traversal constraints, collection spans and set metadata, pane transitions, machine-readable inspection, and built-in semantic audits. Platform accessibility tools remain the final verification step because screen readers can apply platform-specific presentation rules.
+
+For underlying platform guidance, see the Apple https://developer.apple.com/documentation/uikit/uiaccessibility-protocol[UIAccessibility protocol], Android https://developer.android.com/guide/topics/ui/accessibility/principles[accessibility principles], Microsoft https://learn.microsoft.com/windows/win32/winauto/uiauto-providersoverview[UI Automation provider overview], and W3C https://www.w3.org/TR/wai-aria-1.2/[WAI-ARIA].
diff --git a/docs/developer-guide/developer-guide.asciidoc b/docs/developer-guide/developer-guide.asciidoc
index b1c598cf5c2..8f9ab5b8474 100644
--- a/docs/developer-guide/developer-guide.asciidoc
+++ b/docs/developer-guide/developer-guide.asciidoc
@@ -57,6 +57,8 @@ include::css.asciidoc[]
include::The-Components-Of-Codename-One.asciidoc[]
+include::Accessibility-Semantics.asciidoc[]
+
include::Media-And-Audio.asciidoc[]
include::Animations.asciidoc[]
diff --git a/maven/core-unittests/src/test/java/com/codename1/ui/accessibility/AccessibilitySemanticsTest.java b/maven/core-unittests/src/test/java/com/codename1/ui/accessibility/AccessibilitySemanticsTest.java
new file mode 100644
index 00000000000..f86fba2f206
--- /dev/null
+++ b/maven/core-unittests/src/test/java/com/codename1/ui/accessibility/AccessibilitySemanticsTest.java
@@ -0,0 +1,182 @@
+package com.codename1.ui.accessibility;
+
+import com.codename1.junit.UITestBase;
+import com.codename1.junit.FormTest;
+import com.codename1.ui.Button;
+import com.codename1.ui.CheckBox;
+import com.codename1.ui.Component;
+import com.codename1.ui.Container;
+import com.codename1.ui.Form;
+import com.codename1.ui.Label;
+import com.codename1.ui.Slider;
+import com.codename1.ui.TextField;
+import com.codename1.ui.list.DefaultListModel;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class AccessibilitySemanticsTest extends UITestBase {
+
+ @FormTest
+ void infersRolesStatesValuesAndActionsForBuiltInControls() {
+ Form form = form();
+ Button button = place(form, new Button("Save"), 10, 10, 100, 30);
+ CheckBox checkbox = place(form, new CheckBox("Remember me"), 10, 50, 140, 30);
+ checkbox.setSelected(true);
+ Slider slider = place(form, new Slider(), 10, 90, 200, 30);
+ slider.setEditable(true);
+ slider.setMinValue(10);
+ slider.setMaxValue(50);
+ slider.setProgress(30);
+ TextField field = place(form, new TextField("hello"), 10, 130, 200, 35);
+
+ AccessibilityTreeSnapshot tree = AccessibilityInspector.snapshot(form);
+ AccessibilityNodeSnapshot buttonNode = find(tree, button);
+ AccessibilityNodeSnapshot checkNode = find(tree, checkbox);
+ AccessibilityNodeSnapshot sliderNode = find(tree, slider);
+ AccessibilityNodeSnapshot fieldNode = find(tree, field);
+
+ assertEquals(AccessibilityRole.BUTTON, buttonNode.getRole());
+ assertEquals("Save", buttonNode.getLabel());
+ assertNotNull(buttonNode.getAction(AccessibilityAction.ACTIVATE));
+ assertEquals(AccessibilityRole.CHECKBOX, checkNode.getRole());
+ assertEquals(AccessibilityCheckedState.CHECKED, checkNode.getChecked());
+ assertEquals(30d, sliderNode.getRange().getCurrent());
+ assertNotNull(sliderNode.getAction(AccessibilityAction.INCREMENT));
+ assertEquals(AccessibilityRole.TEXT_FIELD, fieldNode.getRole());
+ assertEquals("hello", fieldNode.getLabel());
+ assertNotNull(fieldNode.getAction(AccessibilityAction.SET_TEXT));
+ }
+
+ @FormTest
+ void explicitSemanticsOverrideInferredValuesAndLegacyTextMapsToLabel() {
+ Form form = form();
+ Button button = place(form, new Button("Visual"), 10, 10, 100, 30);
+ button.setAccessibilityText("Legacy");
+ assertEquals("Legacy", button.getSemantics().getLabel());
+ button.getSemantics().setLabel("Semantic").setRole(AccessibilityRole.SWITCH)
+ .setChecked(AccessibilityCheckedState.MIXED).setHint("Changes mode");
+
+ AccessibilityNodeSnapshot node = find(AccessibilityInspector.snapshot(form), button);
+ assertEquals("Semantic", node.getLabel());
+ assertEquals("Semantic", button.getAccessibilityText());
+ assertEquals(AccessibilityRole.SWITCH, node.getRole());
+ assertEquals(AccessibilityCheckedState.MIXED, node.getChecked());
+ assertEquals("Changes mode", node.getHint());
+ }
+
+ @FormTest
+ void groupingCanMergeOrPromoteDescendants() {
+ Form form = form();
+ Container merged = place(form, new Container(), 0, 0, 200, 100);
+ Label first = place(merged, new Label("First"), 0, 0, 100, 30);
+ place(merged, new Label("Second"), 0, 35, 100, 30);
+ merged.getSemantics().setRole(AccessibilityRole.GENERIC)
+ .setGrouping(AccessibilityGrouping.MERGE_DESCENDANTS);
+
+ AccessibilityTreeSnapshot tree = AccessibilityInspector.snapshot(form);
+ AccessibilityNodeSnapshot mergedNode = find(tree, merged);
+ assertTrue(mergedNode.getLabel().contains("First"));
+ assertTrue(mergedNode.getLabel().contains("Second"));
+ assertTrue(mergedNode.getChildIds().isEmpty());
+ assertNull(findOrNull(tree, first));
+
+ merged.getSemantics().setGrouping(AccessibilityGrouping.EXCLUDE);
+ tree = AccessibilityInspector.snapshot(form);
+ assertNull(findOrNull(tree, merged));
+ assertNotNull(findOrNull(tree, first));
+ }
+
+ @FormTest
+ void sortKeysProduceExplicitTraversalOrder() {
+ Form form = form();
+ Button second = place(form, new Button("Second"), 0, 0, 100, 30);
+ Button first = place(form, new Button("First"), 0, 40, 100, 30);
+ second.getSemantics().setSortKey(2);
+ first.getSemantics().setSortKey(1);
+
+ AccessibilityTreeSnapshot tree = AccessibilityInspector.snapshot(form);
+ AccessibilityNodeSnapshot formNode = find(tree, form);
+ int firstIndex = formNode.getChildIds().indexOf(Long.valueOf(find(tree, first).getId()));
+ int secondIndex = formNode.getChildIds().indexOf(Long.valueOf(find(tree, second).getId()));
+ assertTrue(firstIndex >= 0 && secondIndex >= 0 && firstIndex < secondIndex);
+ }
+
+ @FormTest
+ void rendererBackedListsExposeStableVirtualCollectionItems() {
+ Form form = form();
+ com.codename1.ui.List list = new com.codename1.ui.List(
+ new DefaultListModel("Alpha", "Beta", "Gamma"));
+ place(form, list, 0, 0, 200, 160);
+ list.setSelectedIndex(1);
+
+ AccessibilityTreeSnapshot first = AccessibilityInspector.snapshot(form);
+ AccessibilityNodeSnapshot listNode = find(first, list);
+ assertEquals(3, listNode.getChildIds().size());
+ AccessibilityNodeSnapshot beta = first.getNode(listNode.getChildIds().get(1).longValue());
+ assertTrue(beta.getLabel().contains("Beta"));
+ assertEquals(Boolean.TRUE, beta.getSelected());
+ assertEquals(2, beta.getCollectionItemInfo().getPositionInSet());
+ long stableId = beta.getId();
+
+ list.setSelectedIndex(2);
+ AccessibilityTreeSnapshot second = AccessibilityInspector.snapshot(form);
+ AccessibilityNodeSnapshot secondList = find(second, list);
+ assertEquals(stableId, secondList.getChildIds().get(1).longValue());
+ assertEquals(Boolean.FALSE, second.getNode(stableId).getSelected());
+ }
+
+ @FormTest
+ void auditsInvalidRangesAndUnlabeledCustomActions() {
+ Form form = form();
+ Component custom = place(form, new Label(), 0, 0, 100, 30);
+ custom.getSemantics().setRole(AccessibilityRole.SLIDER)
+ .setRange(new AccessibilityRange(10, 5, 20, -1))
+ .addAction(new AccessibilityAction("custom", null,
+ new AccessibilityAction.Handler() {
+ public boolean perform(Component component, Object argument) { return true; }
+ }));
+
+ List issues = AccessibilityAssertions.audit(AccessibilityInspector.snapshot(form));
+ assertTrue(hasIssue(issues, "invalid-range"));
+ assertTrue(hasIssue(issues, "unlabeled-custom-action"));
+ assertThrows(AssertionError.class,
+ () -> AccessibilityAssertions.assertNoErrors(AccessibilityInspector.snapshot(form)));
+ }
+
+ private Form form() {
+ Form form = new Form("Accessible form");
+ form.setWidth(400);
+ form.setHeight(600);
+ form.getContentPane().setWidth(400);
+ form.getContentPane().setHeight(600);
+ return form;
+ }
+
+ private T place(Container parent, T component, int x, int y, int w, int h) {
+ parent.add(component);
+ component.setX(x);
+ component.setY(y);
+ component.setWidth(w);
+ component.setHeight(h);
+ return component;
+ }
+
+ private AccessibilityNodeSnapshot find(AccessibilityTreeSnapshot tree, Component component) {
+ AccessibilityNodeSnapshot node = findOrNull(tree, component);
+ assertNotNull(node, "No semantic node for " + component);
+ return node;
+ }
+
+ private AccessibilityNodeSnapshot findOrNull(AccessibilityTreeSnapshot tree, Component component) {
+ for (AccessibilityNodeSnapshot node : tree.getNodes().values()) {
+ if (node.getComponent() == component && node.getVirtualKey() == null) return node;
+ }
+ return null;
+ }
+
+ private boolean hasIssue(List issues, String code) {
+ for (AccessibilityIssue issue : issues) if (code.equals(issue.getCode())) return true;
+ return false;
+ }
+}
diff --git a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/accessibility/AccessibilityTest.java b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/accessibility/AccessibilityTest.java
index 3494bf9f11e..1c243f04a49 100644
--- a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/accessibility/AccessibilityTest.java
+++ b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/accessibility/AccessibilityTest.java
@@ -1,20 +1,245 @@
package com.codenameone.examples.hellocodenameone.tests.accessibility;
+import com.codename1.ui.Button;
+import com.codename1.ui.CN;
+import com.codename1.ui.CheckBox;
+import com.codename1.ui.Component;
+import com.codename1.ui.Container;
import com.codename1.ui.Display;
+import com.codename1.ui.Form;
+import com.codename1.ui.Label;
+import com.codename1.ui.Slider;
+import com.codename1.ui.accessibility.AccessibilityAction;
+import com.codename1.ui.accessibility.AccessibilityAssertions;
+import com.codename1.ui.accessibility.AccessibilityCheckedState;
+import com.codename1.ui.accessibility.AccessibilityCollectionInfo;
+import com.codename1.ui.accessibility.AccessibilityCollectionItemInfo;
+import com.codename1.ui.accessibility.AccessibilityGrouping;
+import com.codename1.ui.accessibility.AccessibilityInspector;
+import com.codename1.ui.accessibility.AccessibilityLiveRegion;
+import com.codename1.ui.accessibility.AccessibilityManager;
+import com.codename1.ui.accessibility.AccessibilityNode;
+import com.codename1.ui.accessibility.AccessibilityNodeSnapshot;
+import com.codename1.ui.accessibility.AccessibilityRange;
+import com.codename1.ui.accessibility.AccessibilityRole;
+import com.codename1.ui.accessibility.AccessibilityTreeSnapshot;
+import com.codename1.ui.geom.Rectangle;
import com.codenameone.examples.hellocodenameone.tests.BaseTest;
+/** Cross-port semantics conformance test. This runs in every native cn1ss job. */
public class AccessibilityTest extends BaseTest {
+ private volatile boolean customActionPerformed;
+
@Override
public boolean runTest() throws Exception {
- com.codename1.ui.CN.callSerially(() -> {
- String expected = "Testing accessibility announcement";
- // Just verify that invoking this doesn't crash the app
- Display.getInstance().announceForAccessibility(expected);
- done();
+ CN.callSerially(new Runnable() {
+ public void run() {
+ try {
+ runSemanticAssertions();
+ } catch (Throwable t) {
+ fail("Accessibility semantic assertions failed: " + t);
+ }
+ }
});
return true;
}
+ private void runSemanticAssertions() {
+ final Form form = new Form("Accessibility conformance");
+ size(form, 0, 0, 600, 1200);
+ size(form.getContentPane(), 0, 0, 600, 1200);
+ form.getSemantics().setPaneTitle("Settings").setRole(AccessibilityRole.GENERIC);
+
+ Label heading = add(form, new Label("Preferences"), 10, 10, 300, 40);
+ heading.getSemantics().setHeadingLevel(2).setIdentifier("preferences-heading");
+
+ Button save = add(form, new Button("Save"), 10, 60, 120, 40);
+ save.getSemantics().setHint("Saves all changes").setSortKey(2)
+ .addAction(new AccessibilityAction("share", "Share settings",
+ new AccessibilityAction.Handler() {
+ public boolean perform(Component component, Object argument) {
+ customActionPerformed = true;
+ return true;
+ }
+ }));
+
+ CheckBox notifications = add(form, new CheckBox("Notifications"), 10, 110, 220, 40);
+ notifications.setSelected(true);
+ notifications.getSemantics().setSortKey(1).setRequired(Boolean.TRUE);
+
+ Slider volume = add(form, new Slider(), 10, 160, 260, 40);
+ volume.setEditable(true);
+ volume.getSemantics().setLabel("Volume")
+ .setRange(new AccessibilityRange(0, 100, 35, 5, "35 percent"));
+
+ Label error = add(form, new Label("Password is too short"), 10, 210, 300, 40);
+ error.getSemantics().setRole(AccessibilityRole.ALERT).setLiveRegion(AccessibilityLiveRegion.ASSERTIVE)
+ .setValidationError("Password is too short").setInvalid(Boolean.TRUE);
+
+ Container collection = add(form, new Container(), 10, 260, 300, 150);
+ collection.getSemantics().setRole(AccessibilityRole.GRID)
+ .setLabel("Accounts")
+ .setCollectionInfo(new AccessibilityCollectionInfo(2, 1, false,
+ AccessibilityCollectionInfo.SELECTION_SINGLE));
+ Label firstRow = add(collection, new Label("Personal"), 0, 0, 250, 50);
+ firstRow.getSemantics().setRole(AccessibilityRole.CELL).setSelected(Boolean.TRUE)
+ .setCollectionItemInfo(new AccessibilityCollectionItemInfo(0, 1, 0, 1, 1, 2, 1, false));
+ Label secondRow = add(collection, new Label("Business"), 0, 55, 250, 50);
+ secondRow.getSemantics().setRole(AccessibilityRole.CELL).setSelected(Boolean.FALSE)
+ .setCollectionItemInfo(new AccessibilityCollectionItemInfo(1, 1, 0, 1, 2, 2, 1, false));
+
+ Container merged = add(form, new Container(), 10, 420, 300, 80);
+ merged.getSemantics().setRole(AccessibilityRole.GENERIC).setGrouping(AccessibilityGrouping.MERGE_DESCENDANTS);
+ add(merged, new Label("Billing"), 0, 0, 100, 30);
+ add(merged, new Label("monthly"), 110, 0, 100, 30);
+
+ Label semanticSwitch = add(form, new Label("Automatic updates"), 10, 510, 260, 40);
+ semanticSwitch.getSemantics().setRole(AccessibilityRole.SWITCH)
+ .setChecked(AccessibilityCheckedState.MIXED).setExpanded(Boolean.TRUE);
+
+ Container semanticList = add(form, new Container(), 10, 560, 300, 90);
+ semanticList.getSemantics().setRole(AccessibilityRole.LIST).setLabel("Recent projects")
+ .setCollectionInfo(new AccessibilityCollectionInfo(2, 1, false,
+ AccessibilityCollectionInfo.SELECTION_NONE));
+ Label semanticListItem = add(semanticList, new Label("Project Aurora"), 0, 0, 250, 35);
+ semanticListItem.getSemantics().setRole(AccessibilityRole.LIST_ITEM)
+ .setCollectionItemInfo(new AccessibilityCollectionItemInfo(1, 2));
+
+ Label semanticTab = add(form, new Label("Overview"), 10, 660, 160, 40);
+ semanticTab.getSemantics().setRole(AccessibilityRole.TAB).setSelected(Boolean.TRUE);
+
+ Container semanticDialog = add(form, new Container(), 10, 710, 300, 90);
+ semanticDialog.getSemantics().setRole(AccessibilityRole.DIALOG).setLabel("Confirm deletion")
+ .setPaneTitle("Confirmation").setModal(true);
+
+ Container virtualHost = add(form, new Container(), 10, 810, 300, 100);
+ virtualHost.getSemantics().setRole(AccessibilityRole.LIST).setLabel("Virtual results")
+ .addChild(new AccessibilityNode("result-42").setRole(AccessibilityRole.LIST_ITEM)
+ .setLabel("Virtual result").setBounds(new Rectangle(0, 0, 280, 45))
+ .setCollectionItemInfo(new AccessibilityCollectionItemInfo(1, 1)));
+
+ Button disabled = add(form, new Button("Unavailable action"), 10, 920, 220, 40);
+ disabled.setEnabled(false);
+
+ AccessibilityTreeSnapshot first = AccessibilityInspector.snapshot(form);
+ AccessibilityNodeSnapshot headingNode = find(first, heading);
+ AccessibilityNodeSnapshot saveNode = find(first, save);
+ AccessibilityNodeSnapshot checkboxNode = find(first, notifications);
+ AccessibilityNodeSnapshot volumeNode = find(first, volume);
+ AccessibilityNodeSnapshot errorNode = find(first, error);
+ AccessibilityNodeSnapshot collectionNode = find(first, collection);
+ AccessibilityNodeSnapshot mergedNode = find(first, merged);
+ AccessibilityNodeSnapshot switchNode = find(first, semanticSwitch);
+ AccessibilityNodeSnapshot listNode = find(first, semanticList);
+ AccessibilityNodeSnapshot tabNode = find(first, semanticTab);
+ AccessibilityNodeSnapshot dialogNode = find(first, semanticDialog);
+ AccessibilityNodeSnapshot virtualHostNode = find(first, virtualHost);
+
+ assertEqual(AccessibilityRole.HEADING, headingNode.getRole(), "heading role");
+ assertEqual(2, headingNode.getHeadingLevel(), "heading level");
+ assertEqual("preferences-heading", headingNode.getIdentifier(), "automation identifier");
+ assertEqual(AccessibilityRole.BUTTON, saveNode.getRole(), "inferred button role");
+ assertTrue(saveNode.getAction(AccessibilityAction.ACTIVATE) != null, "standard activation action");
+ assertTrue(saveNode.getAction("share") != null, "custom action");
+ assertEqual(AccessibilityCheckedState.CHECKED, checkboxNode.getChecked(), "checked state");
+ assertEqual(Boolean.TRUE, checkboxNode.getRequired(), "required state");
+ assertEqual(35.0, volumeNode.getRange().getCurrent(), "range value");
+ assertEqual(5.0, volumeNode.getRange().getStep(), "range step");
+ assertEqual(AccessibilityLiveRegion.ASSERTIVE, errorNode.getLiveRegion(), "live region");
+ assertEqual(Boolean.TRUE, errorNode.getInvalid(), "validation state");
+ assertEqual(2, collectionNode.getCollectionInfo().getRowCount(), "collection rows");
+ assertEqual(2, collectionNode.getChildIds().size(), "collection children");
+ assertEqual(2, find(first, secondRow).getCollectionItemInfo().getPositionInSet(), "set position");
+ assertTrue(mergedNode.getLabel().indexOf("Billing") >= 0, "merged first label");
+ assertTrue(mergedNode.getLabel().indexOf("monthly") >= 0, "merged second label");
+ assertEqual(0, mergedNode.getChildIds().size(), "merged descendants hidden");
+ assertTrue(findOrNull(first, firstRow) != null, "collection descendant retained");
+ assertTrue(findOrNull(first, merged.getComponentAt(0)) == null, "merged descendant omitted");
+ assertEqual(AccessibilityRole.SWITCH, switchNode.getRole(), "switch role");
+ assertEqual(AccessibilityCheckedState.MIXED, switchNode.getChecked(), "mixed switch state");
+ assertEqual(Boolean.TRUE, switchNode.getExpanded(), "expanded state");
+ assertEqual(AccessibilityRole.LIST, listNode.getRole(), "list role");
+ assertEqual(AccessibilityRole.LIST_ITEM, find(first, semanticListItem).getRole(), "list item role");
+ assertEqual(AccessibilityRole.TAB, tabNode.getRole(), "tab role");
+ assertEqual(Boolean.TRUE, tabNode.getSelected(), "selected tab state");
+ assertEqual(AccessibilityRole.DIALOG, dialogNode.getRole(), "dialog role");
+ assertTrue(dialogNode.isModal(), "modal dialog state");
+ assertEqual("Confirmation", dialogNode.getPaneTitle(), "pane transition title");
+ assertEqual(Boolean.FALSE, find(first, disabled).getEnabled(), "disabled state");
+ assertEqual(1, virtualHostNode.getChildIds().size(), "virtual accessibility descendant");
+ AccessibilityNodeSnapshot virtualNode = first.getNode(virtualHostNode.getChildIds().get(0).longValue());
+ assertEqual("result-42", virtualNode.getVirtualKey(), "stable virtual key");
+ assertEqual(AccessibilityRole.LIST_ITEM, virtualNode.getRole(), "virtual child role");
+ assertEqual("Virtual result", first.getNodeAt(20, 830).getLabel(), "virtual child hit testing");
+
+ AccessibilityNodeSnapshot formNode = find(first, form);
+ int checkboxPosition = formNode.getChildIds().indexOf(Long.valueOf(checkboxNode.getId()));
+ int savePosition = formNode.getChildIds().indexOf(Long.valueOf(saveNode.getId()));
+ assertTrue(checkboxPosition >= 0 && checkboxPosition < savePosition, "explicit traversal order");
+ assertTrue(first.getNodeAt(20, 70) != null, "semantic hit testing");
+ AccessibilityAssertions.assertNoErrors(first);
+ AccessibilityAssertions.assertNoUnlabeledInteractiveNodes(first);
+
+ String json = first.toJson();
+ assertTrue(json.indexOf("\"role\":\"HEADING\"") >= 0, "serialized role");
+ assertTrue(json.indexOf("\"liveRegion\":\"ASSERTIVE\"") >= 0, "serialized live region");
+ assertTrue(json.indexOf("\"collection\"") >= 0, "serialized collection metadata");
+
+ long stableSaveId = saveNode.getId();
+ notifications.setSelected(false);
+ AccessibilityTreeSnapshot second = AccessibilityInspector.snapshot(form);
+ assertEqual(stableSaveId, find(second, save).getId(), "stable semantic id");
+ assertEqual(AccessibilityCheckedState.UNCHECKED, find(second, notifications).getChecked(), "updated state");
+ assertTrue(second.getGeneration() > first.getGeneration(), "tree generation changes");
+
+ assertTrue(Display.getInstance().isAccessibilityTreeSupported(),
+ "native port exposes the virtual accessibility tree");
+ form.show();
+ assertTrue(AccessibilityManager.getInstance().performAction(stableSaveId, "share", null),
+ "custom action accepted");
+ CN.callSerially(new Runnable() {
+ public void run() {
+ try {
+ assertTrue(customActionPerformed, "custom action dispatched on EDT");
+ AccessibilityTreeSnapshot nativeSnapshot = AccessibilityInspector.currentSnapshot();
+ assertEqual(stableSaveId, find(nativeSnapshot, save).getId(),
+ "current native semantic snapshot");
+ AccessibilityAssertions.assertNoErrors(nativeSnapshot);
+ Display.getInstance().announceForAccessibility("Testing accessibility announcement");
+ done();
+ } catch (Throwable t) {
+ fail("Accessibility action assertions failed: " + t);
+ }
+ }
+ });
+ }
+
+ private T add(Container parent, T component, int x, int y, int width, int height) {
+ parent.add(component);
+ size(component, x, y, width, height);
+ return component;
+ }
+
+ private void size(Component component, int x, int y, int width, int height) {
+ component.setX(x);
+ component.setY(y);
+ component.setWidth(width);
+ component.setHeight(height);
+ }
+
+ private AccessibilityNodeSnapshot find(AccessibilityTreeSnapshot tree, Component component) {
+ AccessibilityNodeSnapshot result = findOrNull(tree, component);
+ assertTrue(result != null, "semantic node exists for " + component.getClass().getName());
+ return result;
+ }
+
+ private AccessibilityNodeSnapshot findOrNull(AccessibilityTreeSnapshot tree, Component component) {
+ for (AccessibilityNodeSnapshot node : tree.getNodes().values()) {
+ if (node.getComponent() == component && node.getVirtualKey() == null) return node;
+ }
+ return null;
+ }
+
@Override
public boolean shouldTakeScreenshot() {
return false;
From c56de3fbc91376b8ca71e9ae3ea05e9bf31b423a Mon Sep 17 00:00:00 2001
From: Shai Almog <67850168+shai-almog@users.noreply.github.com>
Date: Sat, 11 Jul 2026 16:57:19 +0300
Subject: [PATCH 02/15] Document accessibility package
---
.../codename1/ui/accessibility/package-info.java | 16 ++++++++++++++++
1 file changed, 16 insertions(+)
create mode 100644 CodenameOne/src/com/codename1/ui/accessibility/package-info.java
diff --git a/CodenameOne/src/com/codename1/ui/accessibility/package-info.java b/CodenameOne/src/com/codename1/ui/accessibility/package-info.java
new file mode 100644
index 00000000000..2168b0b820b
--- /dev/null
+++ b/CodenameOne/src/com/codename1/ui/accessibility/package-info.java
@@ -0,0 +1,16 @@
+/*
+ * Copyright (c) 2026 Codename One and contributors. All rights reserved.
+ */
+
+/// Defines the portable accessibility semantics tree for Codename One's
+/// lightweight user-interface components. Applications use this package to
+/// describe roles, states, values, ranges, actions, traversal, grouping,
+/// live regions, panes, collections, and virtual descendants. Platform ports
+/// translate the resulting immutable tree to their native accessibility APIs.
+///
+/// The entry point for configuring a component is
+/// {@link com.codename1.ui.Component#getSemantics()}. Tests and development
+/// tools can inspect a resolved tree with
+/// {@link com.codename1.ui.accessibility.AccessibilityInspector} and validate
+/// it with {@link com.codename1.ui.accessibility.AccessibilityAssertions}.
+package com.codename1.ui.accessibility;
From 8af86fda2d2c2576ee35c2ad8f3681851db96a68 Mon Sep 17 00:00:00 2001
From: Shai Almog <67850168+shai-almog@users.noreply.github.com>
Date: Sat, 11 Jul 2026 17:07:39 +0300
Subject: [PATCH 03/15] Fix native accessibility compatibility
---
.../android/AndroidAccessibilityProvider.java | 34 ++++++++++++++++---
.../cn1_windows_accessibility.cpp | 2 +-
2 files changed, 30 insertions(+), 6 deletions(-)
diff --git a/Ports/Android/src/com/codename1/impl/android/AndroidAccessibilityProvider.java b/Ports/Android/src/com/codename1/impl/android/AndroidAccessibilityProvider.java
index 045eb526df1..fc9fd853368 100644
--- a/Ports/Android/src/com/codename1/impl/android/AndroidAccessibilityProvider.java
+++ b/Ports/Android/src/com/codename1/impl/android/AndroidAccessibilityProvider.java
@@ -113,11 +113,11 @@ private AccessibilityNodeInfo createVirtualNode(AccessibilityTreeSnapshot tree,
}
if (Build.VERSION.SDK_INT >= 21 && node.getValidationError() != null) info.setError(node.getValidationError());
if (Build.VERSION.SDK_INT >= 28) {
- info.setHeading(node.getHeadingLevel() > 0
- || node.getRole() == AccessibilityRole.HEADING
- || node.getCollectionItemInfo() != null && node.getCollectionItemInfo().isHeading());
- info.setScreenReaderFocusable(node.isFocusable() || label != null);
- if (node.getPaneTitle() != null) info.setPaneTitle(node.getPaneTitle());
+ setApi28Semantics(info,
+ node.getHeadingLevel() > 0
+ || node.getRole() == AccessibilityRole.HEADING
+ || node.getCollectionItemInfo() != null && node.getCollectionItemInfo().isHeading(),
+ node.isFocusable() || label != null, node.getPaneTitle());
}
if (Build.VERSION.SDK_INT >= 19) {
info.setLiveRegion(node.getLiveRegion() == AccessibilityLiveRegion.ASSERTIVE
@@ -131,6 +131,30 @@ private AccessibilityNodeInfo createVirtualNode(AccessibilityTreeSnapshot tree,
return info;
}
+ /**
+ * Invokes API 28 node properties without requiring the Android port's
+ * compile-time SDK stub to expose them. Release builds may compile the
+ * port against an older android.jar even though these calls run only on
+ * Android 9 and newer.
+ */
+ private static void setApi28Semantics(AccessibilityNodeInfo info, boolean heading,
+ boolean screenReaderFocusable, String paneTitle) {
+ try {
+ Class type = AccessibilityNodeInfo.class;
+ type.getMethod("setHeading", new Class[]{Boolean.TYPE})
+ .invoke(info, new Object[]{Boolean.valueOf(heading)});
+ type.getMethod("setScreenReaderFocusable", new Class[]{Boolean.TYPE})
+ .invoke(info, new Object[]{Boolean.valueOf(screenReaderFocusable)});
+ if (paneTitle != null) {
+ type.getMethod("setPaneTitle", new Class[]{CharSequence.class})
+ .invoke(info, new Object[]{paneTitle});
+ }
+ } catch (Throwable ignored) {
+ // The runtime API is authoritative; gracefully omit these optional
+ // properties on vendor builds that don't expose the API 28 methods.
+ }
+ }
+
private void addActions(AccessibilityNodeInfo info, AccessibilityNodeSnapshot node) {
if (node.isFocusable()) info.addAction(AccessibilityNodeInfo.ACTION_FOCUS);
if (hasAction(node, AccessibilityAction.ACTIVATE)) info.addAction(AccessibilityNodeInfo.ACTION_CLICK);
diff --git a/Ports/WindowsPort/nativeSources/cn1_windows_accessibility.cpp b/Ports/WindowsPort/nativeSources/cn1_windows_accessibility.cpp
index d70bb3d7976..333b29d92b1 100644
--- a/Ports/WindowsPort/nativeSources/cn1_windows_accessibility.cpp
+++ b/Ports/WindowsPort/nativeSources/cn1_windows_accessibility.cpp
@@ -258,7 +258,7 @@ JAVA_VOID com_codename1_impl_windows_WindowsNative_accessibilityAction___long_ja
JAVA_VOID com_codename1_impl_windows_WindowsNative_accessibilityEnd___int(CODENAME_ONE_THREAD_STATE, JAVA_INT changeType) {
AcquireSRWLockExclusive(&cn1UiaLock); cn1UiaNodes.swap(cn1UiaPending); cn1UiaPending.clear(); ReleaseSRWLockExclusive(&cn1UiaLock);
CN1UiaProvider* root=new CN1UiaProvider(0);
- StructureChangedType type=(changeType&1)?StructureChangeType_ChildrenInvalidated:StructureChangeType_ChildrenReordered;
+ StructureChangeType type=(changeType&1)?StructureChangeType_ChildrenInvalidated:StructureChangeType_ChildrenReordered;
UiaRaiseStructureChangedEvent(root,type,NULL,0); root->Release();
}
}
From 0d5d20f294b5302929c713774652a0f90d780673 Mon Sep 17 00:00:00 2001
From: Shai Almog <67850168+shai-almog@users.noreply.github.com>
Date: Sat, 11 Jul 2026 18:49:08 +0300
Subject: [PATCH 04/15] Fix accessibility CI quality gates
---
.../accessibility/AccessibilityManager.java | 184 +++++++++++-------
.../cn1_windows_accessibility.cpp | 9 +-
.../AccessibilitySemanticsSnippets.java | 156 +++++++++++++++
.../Accessibility-Semantics.asciidoc | 99 +---------
4 files changed, 283 insertions(+), 165 deletions(-)
create mode 100644 docs/demos/common/src/main/java/com/codenameone/developerguide/snippets/AccessibilitySemanticsSnippets.java
diff --git a/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityManager.java b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityManager.java
index 476e826c801..33ccb4d715d 100644
--- a/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityManager.java
+++ b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityManager.java
@@ -241,15 +241,17 @@ private BuildNode buildComponentNode(final Component component, AccessibilityNod
out.builder.collectionItemInfo = config.getCollectionItemInfo() == null
? inferCollectionItem(component) : config.getCollectionItemInfo();
out.builder.bounds = componentBounds(component);
- out.builder.selected = config.getSelected() == null ? inferSelected(component) : config.getSelected();
+ out.builder.selected = config.getSelected();
+ if (out.builder.selected == null) applyInferredSelected(component, out.builder);
out.builder.expanded = config.getExpanded();
out.builder.enabled = config.getEnabled() == null ? Boolean.valueOf(component.isEnabled()) : config.getEnabled();
out.builder.invalid = config.getInvalid();
out.builder.busy = config.getBusy();
- out.builder.readOnly = config.getReadOnly() == null ? inferReadOnly(component) : config.getReadOnly();
+ out.builder.readOnly = config.getReadOnly();
out.builder.required = config.getRequired();
- out.builder.multiline = config.getMultiline() == null ? inferMultiline(component) : config.getMultiline();
- out.builder.obscured = config.getObscured() == null ? inferObscured(component) : config.getObscured();
+ out.builder.multiline = config.getMultiline();
+ out.builder.obscured = config.getObscured();
+ applyInferredTextStates(component, out.builder);
out.builder.pressed = config.getPressed();
out.builder.current = config.getCurrent();
out.builder.modal = config.isModal() || component instanceof Dialog;
@@ -339,14 +341,7 @@ private void addListChildren(final com.codename1.ui.List list, long hostId, List
i + 1, size, 1, false))
.setBounds(list.getAccessibilityItemBounds(i, new Rectangle()))
.addAction(new AccessibilityAction(AccessibilityAction.ACTIVATE, null,
- new AccessibilityAction.Handler() {
- public boolean perform(Component component, Object argument) {
- if (!list.isEnabled()) return false;
- list.setSelectedIndex(index);
- list.keyReleased(Display.getInstance().getKeyCode(Display.GAME_FIRE));
- return true;
- }
- }));
+ new ListActivateHandler(list, index)));
destination.add(buildVirtualNode(list, item, "list/item-" + i));
}
}
@@ -354,65 +349,32 @@ public boolean perform(Component component, Object argument) {
private void addDefaultActions(final Component component, AccessibilityNodeSnapshot.Builder builder) {
if (component.isFocusable() && !hasAction(builder.actions, AccessibilityAction.FOCUS)) {
builder.actions.add(new AccessibilityAction(AccessibilityAction.FOCUS, null,
- new AccessibilityAction.Handler() {
- public boolean perform(Component source, Object argument) {
- if (!source.isEnabled()) return false;
- source.requestFocus();
- return true;
- }
- }));
+ FocusHandler.INSTANCE));
}
if (component instanceof Button && !hasAction(builder.actions, AccessibilityAction.ACTIVATE)) {
builder.actions.add(new AccessibilityAction(AccessibilityAction.ACTIVATE, null,
- new AccessibilityAction.Handler() {
- public boolean perform(Component source, Object argument) {
- if (!source.isEnabled()) return false;
- source.keyReleased(Display.getInstance().getKeyCode(Display.GAME_FIRE));
- return true;
- }
- }));
+ ActivateHandler.INSTANCE));
}
if (component instanceof Slider) {
final Slider slider = (Slider)component;
if (slider.isEditable() && !hasAction(builder.actions, AccessibilityAction.INCREMENT)) {
builder.actions.add(new AccessibilityAction(AccessibilityAction.INCREMENT, null,
- new AccessibilityAction.Handler() {
- public boolean perform(Component source, Object argument) {
- slider.setProgress(Math.min(slider.getMaxValue(), slider.getProgress() + Math.max(1, slider.getIncrements())));
- return true;
- }
- }));
+ new SliderAdjustmentHandler(slider, 1)));
}
if (slider.isEditable() && !hasAction(builder.actions, AccessibilityAction.DECREMENT)) {
builder.actions.add(new AccessibilityAction(AccessibilityAction.DECREMENT, null,
- new AccessibilityAction.Handler() {
- public boolean perform(Component source, Object argument) {
- slider.setProgress(Math.max(slider.getMinValue(), slider.getProgress() - Math.max(1, slider.getIncrements())));
- return true;
- }
- }));
+ new SliderAdjustmentHandler(slider, -1)));
}
}
if (component instanceof TextArea) {
final TextArea text = (TextArea)component;
if (!hasAction(builder.actions, AccessibilityAction.FOCUS)) {
builder.actions.add(new AccessibilityAction(AccessibilityAction.FOCUS, null,
- new AccessibilityAction.Handler() {
- public boolean perform(Component source, Object argument) {
- source.requestFocus();
- return true;
- }
- }));
+ FocusHandler.INSTANCE));
}
if (text.isEditable() && !hasAction(builder.actions, AccessibilityAction.SET_TEXT)) {
builder.actions.add(new AccessibilityAction(AccessibilityAction.SET_TEXT, null,
- new AccessibilityAction.Handler() {
- public boolean perform(Component source, Object argument) {
- if (!(argument instanceof String)) return false;
- text.setText((String)argument);
- return true;
- }
- }));
+ new SetTextHandler(text)));
}
}
}
@@ -460,13 +422,13 @@ private AccessibilityCheckedState inferChecked(Component component, Accessibilit
return AccessibilityCheckedState.UNSPECIFIED;
}
- private Boolean inferSelected(Component component) {
+ private void applyInferredSelected(Component component, AccessibilityNodeSnapshot.Builder builder) {
if (component instanceof Button && (((Button)component).isToggle() || isTabButton((Button)component))) {
- return Boolean.valueOf(((Button)component).isSelected());
+ builder.selected = Boolean.valueOf(((Button)component).isSelected());
+ return;
}
Tabs tabOwner = tabPanelOwner(component);
- if (tabOwner != null) return Boolean.valueOf(tabOwner.getSelectedComponent() == component);
- return null;
+ if (tabOwner != null) builder.selected = Boolean.valueOf(tabOwner.getSelectedComponent() == component);
}
private Tabs tabPanelOwner(Component component) {
@@ -515,16 +477,16 @@ private AccessibilityCollectionItemInfo inferCollectionItem(Component component)
return null;
}
- private Boolean inferReadOnly(Component component) {
- return component instanceof TextArea ? Boolean.valueOf(!((TextArea)component).isEditable()) : null;
- }
-
- private Boolean inferMultiline(Component component) {
- return component instanceof TextArea ? Boolean.valueOf(!(component instanceof TextField) || ((TextArea)component).getRows() > 1) : null;
- }
-
- private Boolean inferObscured(Component component) {
- return component instanceof TextArea ? Boolean.valueOf((((TextArea)component).getConstraint() & TextArea.PASSWORD) != 0) : null;
+ private void applyInferredTextStates(Component component, AccessibilityNodeSnapshot.Builder builder) {
+ if (!(component instanceof TextArea)) return;
+ TextArea text = (TextArea)component;
+ if (builder.readOnly == null) builder.readOnly = Boolean.valueOf(!text.isEditable());
+ if (builder.multiline == null) {
+ builder.multiline = Boolean.valueOf(!(component instanceof TextField) || text.getRows() > 1);
+ }
+ if (builder.obscured == null) {
+ builder.obscured = Boolean.valueOf((text.getConstraint() & TextArea.PASSWORD) != 0);
+ }
}
private boolean shouldExpose(Component component, AccessibilityNode config, BuildNode node) {
@@ -565,16 +527,7 @@ private void intersect(Rectangle target, Rectangle clip) {
}
private void sortTree(List nodes) {
- Collections.sort(nodes, new Comparator() {
- public int compare(BuildNode a, BuildNode b) {
- boolean an = Double.isNaN(a.builder.sortKey);
- boolean bn = Double.isNaN(b.builder.sortKey);
- if (an && bn) return 0;
- if (an) return 1;
- if (bn) return -1;
- return a.builder.sortKey < b.builder.sortKey ? -1 : a.builder.sortKey == b.builder.sortKey ? 0 : 1;
- }
- });
+ Collections.sort(nodes, SortKeyComparator.INSTANCE);
applyRelativeOrder(nodes);
for (BuildNode node : nodes) sortTree(node.children);
}
@@ -666,4 +619,85 @@ private static final class BuildNode {
AccessibilityNodeSnapshot.Builder builder = new AccessibilityNodeSnapshot.Builder();
List children = new ArrayList();
}
+
+ private static final class SortKeyComparator implements Comparator {
+ private static final SortKeyComparator INSTANCE = new SortKeyComparator();
+
+ public int compare(BuildNode a, BuildNode b) {
+ boolean an = Double.isNaN(a.builder.sortKey);
+ boolean bn = Double.isNaN(b.builder.sortKey);
+ if (an && bn) return 0;
+ if (an) return 1;
+ if (bn) return -1;
+ return Double.compare(a.builder.sortKey, b.builder.sortKey);
+ }
+ }
+
+ private static final class FocusHandler implements AccessibilityAction.Handler {
+ private static final FocusHandler INSTANCE = new FocusHandler();
+
+ public boolean perform(Component source, Object argument) {
+ if (!source.isEnabled()) return false;
+ source.requestFocus();
+ return true;
+ }
+ }
+
+ private static final class ActivateHandler implements AccessibilityAction.Handler {
+ private static final ActivateHandler INSTANCE = new ActivateHandler();
+
+ public boolean perform(Component source, Object argument) {
+ if (!source.isEnabled()) return false;
+ source.keyReleased(Display.getInstance().getKeyCode(Display.GAME_FIRE));
+ return true;
+ }
+ }
+
+ private static final class ListActivateHandler implements AccessibilityAction.Handler {
+ private final com.codename1.ui.List list;
+ private final int index;
+
+ private ListActivateHandler(com.codename1.ui.List list, int index) {
+ this.list = list;
+ this.index = index;
+ }
+
+ public boolean perform(Component source, Object argument) {
+ if (!list.isEnabled()) return false;
+ list.setSelectedIndex(index);
+ list.keyReleased(Display.getInstance().getKeyCode(Display.GAME_FIRE));
+ return true;
+ }
+ }
+
+ private static final class SliderAdjustmentHandler implements AccessibilityAction.Handler {
+ private final Slider slider;
+ private final int direction;
+
+ private SliderAdjustmentHandler(Slider slider, int direction) {
+ this.slider = slider;
+ this.direction = direction;
+ }
+
+ public boolean perform(Component source, Object argument) {
+ int increment = Math.max(1, slider.getIncrements());
+ int value = slider.getProgress() + direction * increment;
+ slider.setProgress(Math.max(slider.getMinValue(), Math.min(slider.getMaxValue(), value)));
+ return true;
+ }
+ }
+
+ private static final class SetTextHandler implements AccessibilityAction.Handler {
+ private final TextArea text;
+
+ private SetTextHandler(TextArea text) {
+ this.text = text;
+ }
+
+ public boolean perform(Component source, Object argument) {
+ if (!(argument instanceof String)) return false;
+ text.setText((String)argument);
+ return true;
+ }
+ }
}
diff --git a/Ports/WindowsPort/nativeSources/cn1_windows_accessibility.cpp b/Ports/WindowsPort/nativeSources/cn1_windows_accessibility.cpp
index 333b29d92b1..bc0d40fabc2 100644
--- a/Ports/WindowsPort/nativeSources/cn1_windows_accessibility.cpp
+++ b/Ports/WindowsPort/nativeSources/cn1_windows_accessibility.cpp
@@ -1,13 +1,20 @@
/* Native virtual accessibility tree for the lightweight Win32 port. */
#ifdef _WIN32
-#include "cn1_windows.h"
+/* xwin may provide a newer MSVC STL than the clang-cl installed by the Linux
+ * cross-compile runner. This file uses only the long-stable string/vector
+ * subset, so allow that supported cross-toolchain pairing. */
+#ifndef _ALLOW_COMPILER_AND_STL_VERSION_MISMATCH
+#define _ALLOW_COMPILER_AND_STL_VERSION_MISMATCH
+#endif
+
#include
#include
#include
#include
#include
#include
+#include "cn1_windows.h"
#pragma comment(lib, "uiautomationcore.lib")
#pragma comment(lib, "oleaut32.lib")
diff --git a/docs/demos/common/src/main/java/com/codenameone/developerguide/snippets/AccessibilitySemanticsSnippets.java b/docs/demos/common/src/main/java/com/codenameone/developerguide/snippets/AccessibilitySemanticsSnippets.java
new file mode 100644
index 00000000000..e678f470ef7
--- /dev/null
+++ b/docs/demos/common/src/main/java/com/codenameone/developerguide/snippets/AccessibilitySemanticsSnippets.java
@@ -0,0 +1,156 @@
+package com.codenameone.developerguide.snippets;
+
+import com.codename1.ui.Button;
+import com.codename1.ui.Component;
+import com.codename1.ui.Form;
+import com.codename1.ui.Label;
+import com.codename1.ui.accessibility.AccessibilityAction;
+import com.codename1.ui.accessibility.AccessibilityAssertions;
+import com.codename1.ui.accessibility.AccessibilityCheckedState;
+import com.codename1.ui.accessibility.AccessibilityChildProvider;
+import com.codename1.ui.accessibility.AccessibilityCollectionInfo;
+import com.codename1.ui.accessibility.AccessibilityCollectionItemInfo;
+import com.codename1.ui.accessibility.AccessibilityGrouping;
+import com.codename1.ui.accessibility.AccessibilityInspector;
+import com.codename1.ui.accessibility.AccessibilityLiveRegion;
+import com.codename1.ui.accessibility.AccessibilityNode;
+import com.codename1.ui.accessibility.AccessibilityNodeSnapshot;
+import com.codename1.ui.accessibility.AccessibilityRange;
+import com.codename1.ui.accessibility.AccessibilityRole;
+import com.codename1.ui.accessibility.AccessibilityTreeSnapshot;
+import com.codename1.ui.geom.Rectangle;
+import java.util.ArrayList;
+import java.util.List;
+
+/** Compiled source snippets for the accessibility semantics guide chapter. */
+public class AccessibilitySemanticsSnippets {
+
+ public void configureButton() {
+ // tag::accessibility-semantics-button[]
+ Button save = new Button("Save");
+ save.getSemantics()
+ .setHint("Saves the edited profile")
+ .setIdentifier("profile-save");
+ // end::accessibility-semantics-button[]
+ }
+
+ public void configureRoles(Component wifiSwitch, Label title) {
+ // tag::accessibility-semantics-roles[]
+ wifiSwitch.getSemantics()
+ .setRole(AccessibilityRole.SWITCH)
+ .setLabel("Wi-Fi")
+ .setChecked(AccessibilityCheckedState.CHECKED)
+ .setEnabled(Boolean.TRUE)
+ .setHint("Double tap to turn Wi-Fi off");
+
+ title.getSemantics()
+ .setHeadingLevel(2)
+ .setIdentifier("network-heading");
+ // end::accessibility-semantics-roles[]
+ }
+
+ public void configureRange(Component rating) {
+ // tag::accessibility-semantics-range[]
+ rating.getSemantics()
+ .setRole(AccessibilityRole.SLIDER)
+ .setLabel("Rating")
+ .setRange(new AccessibilityRange(0, 5, 3, 1, "3 out of 5"));
+ // end::accessibility-semantics-range[]
+ }
+
+ public void configureAction(Component card) {
+ // tag::accessibility-semantics-action[]
+ card.getSemantics().addAction(new AccessibilityAction(
+ "archive",
+ "Archive message",
+ new AccessibilityAction.Handler() {
+ public boolean perform(Component component, Object argument) {
+ archiveMessage();
+ return true;
+ }
+ }
+ ));
+ // end::accessibility-semantics-action[]
+ }
+
+ public void configureGrouping(Component summaryCard) {
+ // tag::accessibility-semantics-grouping[]
+ summaryCard.getSemantics()
+ .setRole(AccessibilityRole.GENERIC)
+ .setGrouping(AccessibilityGrouping.MERGE_DESCENDANTS);
+ // end::accessibility-semantics-grouping[]
+ }
+
+ public void configureVirtualChildren(Component chart, final List points) {
+ // tag::accessibility-semantics-virtual[]
+ chart.getSemantics().setChildProvider(new AccessibilityChildProvider() {
+ public List getAccessibilityChildren(Component owner) {
+ List result = new ArrayList();
+ for (ChartPoint point : points) {
+ AccessibilityNode node = new AccessibilityNode("point-" + point.getId());
+ node.setRole(AccessibilityRole.IMAGE)
+ .setLabel(point.getLabel())
+ .setValue(point.getFormattedValue())
+ .setBounds(point.getBounds());
+ result.add(node);
+ }
+ return result;
+ }
+ });
+ // end::accessibility-semantics-virtual[]
+ }
+
+ public void configureTraversal(Component cancel, Component continueButton, Component help) {
+ // tag::accessibility-semantics-traversal[]
+ cancel.getSemantics().setSortKey(1);
+ continueButton.getSemantics().setSortKey(2);
+ help.getSemantics().setTraversalAfter(continueButton);
+ // end::accessibility-semantics-traversal[]
+ }
+
+ public void configureLiveRegion(Label status) {
+ // tag::accessibility-semantics-live[]
+ status.getSemantics()
+ .setRole(AccessibilityRole.ALERT)
+ .setLiveRegion(AccessibilityLiveRegion.POLITE);
+ status.setText("Upload complete");
+ // end::accessibility-semantics-live[]
+ }
+
+ public void configureCollection(Component grid, Component cell) {
+ // tag::accessibility-semantics-collection[]
+ grid.getSemantics().setCollectionInfo(
+ new AccessibilityCollectionInfo(20, 3, false,
+ AccessibilityCollectionInfo.SELECTION_MULTIPLE));
+
+ cell.getSemantics().setCollectionItemInfo(
+ new AccessibilityCollectionItemInfo(
+ 4, 1, 2, 1, 5, 20, 1, false));
+ // end::accessibility-semantics-collection[]
+ }
+
+ public String inspect(Form myForm, int screenX, int screenY) {
+ // tag::accessibility-semantics-inspection[]
+ AccessibilityTreeSnapshot tree =
+ AccessibilityInspector.snapshot(myForm);
+
+ AccessibilityAssertions.assertNoErrors(tree);
+ AccessibilityAssertions.assertNoUnlabeledInteractiveNodes(tree);
+
+ String diagnosticJson = tree.toJson();
+ AccessibilityNodeSnapshot hit = tree.getNodeAt(screenX, screenY);
+ // end::accessibility-semantics-inspection[]
+ return diagnosticJson + (hit == null ? "" : hit.getLabel());
+ }
+
+ private void archiveMessage() {
+ }
+
+ /** Minimal model contract used by the virtual-descendant example. */
+ public interface ChartPoint {
+ String getId();
+ String getLabel();
+ String getFormattedValue();
+ Rectangle getBounds();
+ }
+}
diff --git a/docs/developer-guide/Accessibility-Semantics.asciidoc b/docs/developer-guide/Accessibility-Semantics.asciidoc
index 6cd09e7f876..a6132715d6b 100644
--- a/docs/developer-guide/Accessibility-Semantics.asciidoc
+++ b/docs/developer-guide/Accessibility-Semantics.asciidoc
@@ -11,12 +11,7 @@ Standard components work without extra configuration. `Button`, `CheckBox`, `Rad
`Component.setAccessibilityText()` remains supported. It is a compatibility alias for the semantic label. New code should use `getSemantics()` when it needs more than a label.
[source,java]
-----
-Button save = new Button("Save");
-save.getSemantics()
- .setHint("Saves the edited profile")
- .setIdentifier("profile-save");
-----
+include::../demos/common/src/main/java/com/codenameone/developerguide/snippets/AccessibilitySemanticsSnippets.java[tag=accessibility-semantics-button,indent=0]
Changes to text, selection, focus, enabled state, bounds, scrolling, ranges, tabs, and the current `Form` automatically invalidate the native semantic tree. Custom components should call `accessibilityChanged()` after a semantic value changes outside a standard setter.
@@ -25,20 +20,7 @@ Changes to text, selection, focus, enabled state, bounds, scrolling, ranges, tab
`AccessibilityRole` contains portable control roles including buttons, toggle buttons, switches, checkboxes, radio buttons, headings, links, images, text and search fields, sliders, progress indicators, lists, grids, rows, cells, headers, tabs, dialogs, alerts, menus, toolbars, combo boxes, trees, and separators. A port maps an unsupported role to the closest native role without discarding its label or state.
[source,java]
-----
-Component wifiSwitch = new MyPaintedSwitch();
-wifiSwitch.getSemantics()
- .setRole(AccessibilityRole.SWITCH)
- .setLabel("Wi-Fi")
- .setChecked(AccessibilityCheckedState.CHECKED)
- .setEnabled(Boolean.TRUE)
- .setHint("Double tap to turn Wi-Fi off");
-
-Label title = new Label("Network");
-title.getSemantics()
- .setHeadingLevel(2)
- .setIdentifier("network-heading");
-----
+include::../demos/common/src/main/java/com/codenameone/developerguide/snippets/AccessibilitySemanticsSnippets.java[tag=accessibility-semantics-roles,indent=0]
Tri-state controls use `UNCHECKED`, `CHECKED`, or `MIXED`. Nullable Boolean properties distinguish an explicit state from an unspecified state. Available properties include selected, expanded, enabled, invalid, busy, read-only, required, multiline, obscured, pressed, and current. `setValidationError()` supplies both the error message and invalid state. `setPaneTitle()` describes a newly displayed pane or screen, and `setModal(true)` identifies a modal scope.
@@ -49,12 +31,7 @@ Use `setRoleDescription()` only when the platform role is not sufficiently speci
`AccessibilityRange` describes minimum, maximum, current value, increment, and an optional spoken value. The spoken value is useful for ranges such as ratings and durations where a raw number is ambiguous.
[source,java]
-----
-rating.getSemantics()
- .setRole(AccessibilityRole.SLIDER)
- .setLabel("Rating")
- .setRange(new AccessibilityRange(0, 5, 3, 1, "3 out of 5"));
-----
+include::../demos/common/src/main/java/com/codenameone/developerguide/snippets/AccessibilitySemanticsSnippets.java[tag=accessibility-semantics-range,indent=0]
Editable controls can provide `SET_VALUE` or `SET_TEXT` actions. Sliders and other adjustable controls should expose `INCREMENT` and `DECREMENT`. Standard components infer these actions.
@@ -63,18 +40,7 @@ Editable controls can provide `SET_VALUE` or `SET_TEXT` actions. Sliders and oth
An `AccessibilityAction` has a stable ID, a localized label, an enabled state, and a handler. Handlers always execute on the Codename One EDT, including when the action originates on a native accessibility thread.
[source,java]
-----
-card.getSemantics().addAction(new AccessibilityAction(
- "archive",
- "Archive message",
- new AccessibilityAction.Handler() {
- public boolean perform(Component component, Object argument) {
- archiveMessage();
- return true;
- }
- }
-));
-----
+include::../demos/common/src/main/java/com/codenameone/developerguide/snippets/AccessibilitySemanticsSnippets.java[tag=accessibility-semantics-action,indent=0]
Use the standard IDs in `AccessibilityAction` for activation, long press, increment, decrement, set value, set text, selection, character or word cursor movement, focus, show-on-screen, dismiss, expand, collapse, scrolling, and clipboard operations. A custom ID must be stable and its label must not be null.
@@ -90,31 +56,12 @@ Use the standard IDs in `AccessibilityAction` for activation, long press, increm
* `EXCLUDE_SUBTREE` removes the component and all descendants.
[source,java]
-----
-summaryCard.getSemantics()
- .setRole(AccessibilityRole.GENERIC)
- .setGrouping(AccessibilityGrouping.MERGE_DESCENDANTS);
-----
+include::../demos/common/src/main/java/com/codenameone/developerguide/snippets/AccessibilitySemanticsSnippets.java[tag=accessibility-semantics-grouping,indent=0]
For content that is painted without child components, attach an `AccessibilityChildProvider`, or add standalone `AccessibilityNode` children. Every virtual node must use a stable `virtualKey`. Stable keys preserve native focus while rows are updated or recycled.
[source,java]
-----
-chart.getSemantics().setChildProvider(new AccessibilityChildProvider() {
- public List getAccessibilityChildren(Component owner) {
- List result = new ArrayList();
- for (Point point : model.getPoints()) {
- AccessibilityNode node = new AccessibilityNode("point-" + point.getId());
- node.setRole(AccessibilityRole.IMAGE)
- .setLabel(point.getLabel())
- .setValue(point.getFormattedValue())
- .setBounds(point.getBounds());
- result.add(node);
- }
- return result;
- }
-});
-----
+include::../demos/common/src/main/java/com/codenameone/developerguide/snippets/AccessibilitySemanticsSnippets.java[tag=accessibility-semantics-virtual,indent=0]
Virtual bounds are relative to the owning component. Component nodes use their clipped absolute bounds.
@@ -123,11 +70,7 @@ Virtual bounds are relative to the owning component. Component nodes use their c
Visual order is the default reading order. `setSortKey()` assigns an ordered semantic key among siblings. `setTraversalBefore()` and `setTraversalAfter()` express a direct relationship when a numeric order would be fragile.
[source,java]
-----
-cancel.getSemantics().setSortKey(1);
-continueButton.getSemantics().setSortKey(2);
-help.getSemantics().setTraversalAfter(continueButton);
-----
+include::../demos/common/src/main/java/com/codenameone/developerguide/snippets/AccessibilitySemanticsSnippets.java[tag=accessibility-semantics-traversal,indent=0]
Keep traversal relationships within the same semantic parent. The tree builder resolves these constraints deterministically and preserves stable node IDs.
@@ -136,12 +79,7 @@ Keep traversal relationships within the same semantic parent. The tree builder r
Set `AccessibilityLiveRegion.POLITE` for non-urgent updates and `ASSERTIVE` for errors or urgent status. `OFF` is the default. Live changes produce the corresponding platform notification or ARIA live update.
[source,java]
-----
-status.getSemantics()
- .setRole(AccessibilityRole.ALERT)
- .setLiveRegion(AccessibilityLiveRegion.POLITE);
-status.setText("Upload complete");
-----
+include::../demos/common/src/main/java/com/codenameone/developerguide/snippets/AccessibilitySemanticsSnippets.java[tag=accessibility-semantics-live,indent=0]
Form changes and nodes with a pane title produce screen or pane transition notifications. `Display.announceForAccessibility()` remains available for an exceptional announcement that is not represented by persistent UI. Prefer a live region for state that is visible on screen.
@@ -150,15 +88,7 @@ Form changes and nodes with a pane title produce screen or pane transition notif
`AccessibilityCollectionInfo` describes row and column counts, hierarchy, and selection mode. `AccessibilityCollectionItemInfo` describes row and column index, spans, position and size in a set, nesting level, and header status.
[source,java]
-----
-grid.getSemantics().setCollectionInfo(
- new AccessibilityCollectionInfo(20, 3, false,
- AccessibilityCollectionInfo.SELECTION_MULTIPLE));
-
-cell.getSemantics().setCollectionItemInfo(
- new AccessibilityCollectionItemInfo(
- 4, 1, 2, 1, 5, 20, 1, false));
-----
+include::../demos/common/src/main/java/com/codenameone/developerguide/snippets/AccessibilitySemanticsSnippets.java[tag=accessibility-semantics-collection,indent=0]
Row and column indexes are zero-based. Position and set size are one-based; use `-1` when unknown. Lists, tables, tabs, and their items infer this metadata where possible.
@@ -167,16 +97,7 @@ Row and column indexes are zero-based. Position and set size are one-based; use
`AccessibilityInspector` returns an immutable snapshot. A snapshot is safe to inspect without racing native accessibility callbacks and includes stable IDs, hierarchy, bounds, resolved state, actions, range and collection data. `toJson()` is suitable for diagnostic reports and tooling.
[source,java]
-----
-AccessibilityTreeSnapshot tree =
- AccessibilityInspector.snapshot(myForm);
-
-AccessibilityAssertions.assertNoErrors(tree);
-AccessibilityAssertions.assertNoUnlabeledInteractiveNodes(tree);
-
-String diagnosticJson = tree.toJson();
-AccessibilityNodeSnapshot hit = tree.getNodeAt(screenX, screenY);
-----
+include::../demos/common/src/main/java/com/codenameone/developerguide/snippets/AccessibilitySemanticsSnippets.java[tag=accessibility-semantics-inspection,indent=0]
`AccessibilityAssertions.audit()` reports machine-readable `AccessibilityIssue` values for unlabeled controls, invalid ranges, duplicate identifiers or actions, invalid collection positions, missing parents, cycles, unnamed dialogs, and unlabeled custom actions. Warnings also identify suspicious role/state combinations and empty bounds.
From 5433112d56d6d0594b13ff84ac43acba9aec7790 Mon Sep 17 00:00:00 2001
From: Shai Almog <67850168+shai-almog@users.noreply.github.com>
Date: Sat, 11 Jul 2026 21:16:52 +0300
Subject: [PATCH 05/15] Fix remaining accessibility CI checks
---
.../impl/android/AndroidAccessibilityProvider.java | 2 +-
docs/developer-guide/Accessibility-Semantics.asciidoc | 10 +++++-----
docs/developer-guide/languagetool-accept.txt | 3 +++
3 files changed, 9 insertions(+), 6 deletions(-)
diff --git a/Ports/Android/src/com/codename1/impl/android/AndroidAccessibilityProvider.java b/Ports/Android/src/com/codename1/impl/android/AndroidAccessibilityProvider.java
index fc9fd853368..85e86a7d7ba 100644
--- a/Ports/Android/src/com/codename1/impl/android/AndroidAccessibilityProvider.java
+++ b/Ports/Android/src/com/codename1/impl/android/AndroidAccessibilityProvider.java
@@ -42,7 +42,7 @@ final class AndroidAccessibilityProvider extends AccessibilityNodeProvider {
@Override
public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) {
AccessibilityTreeSnapshot tree = implementation.getAccessibilityTreeSnapshot();
- if (virtualViewId == View.NO_ID || virtualViewId == AccessibilityNodeProvider.HOST_VIEW_ID) {
+ if (virtualViewId == AccessibilityNodeProvider.HOST_VIEW_ID) {
return createHostNode(tree);
}
AccessibilityNodeSnapshot node = tree.getNode(virtualViewId);
diff --git a/docs/developer-guide/Accessibility-Semantics.asciidoc b/docs/developer-guide/Accessibility-Semantics.asciidoc
index a6132715d6b..ca3bb43edd5 100644
--- a/docs/developer-guide/Accessibility-Semantics.asciidoc
+++ b/docs/developer-guide/Accessibility-Semantics.asciidoc
@@ -1,6 +1,6 @@
= Accessibility Semantics
-Codename One components are lightweight: they are painted by Codename One instead of being represented by a native widget on every platform. The accessibility semantics API builds an explicit, immutable virtual tree from those components and presents it to VoiceOver, TalkBack, Windows UI Automation, AT-SPI, Java Access Bridge, and web accessibility APIs.
+Codename One components are lightweight: they're painted by Codename One instead of being represented by a native widget on every platform. The accessibility semantics API builds an explicit, immutable virtual tree from those components and presents it to VoiceOver, TalkBack, Windows UI Automation, AT-SPI, Java Access Bridge, and web accessibility APIs.
The semantic tree is separate from the visual tree. This is important for renderer-backed controls, merged cards, decorative content, custom controls, and applications that need a reading order different from paint order.
@@ -8,7 +8,7 @@ The semantic tree is separate from the visual tree. This is important for render
Standard components work without extra configuration. `Button`, `CheckBox`, `RadioButton`, `Slider`, text fields, lists, tables, tabs, labels, dialogs, and containers infer appropriate roles, values, states, collection metadata, and standard actions. Renderer-backed `List` rows are exposed as stable virtual children even though no `Component` exists for each painted row.
-`Component.setAccessibilityText()` remains supported. It is a compatibility alias for the semantic label. New code should use `getSemantics()` when it needs more than a label.
+`Component.setAccessibilityText()` remains supported. It's a compatibility alias for the semantic label. New code should use `getSemantics()` when it needs more than a label.
[source,java]
include::../demos/common/src/main/java/com/codenameone/developerguide/snippets/AccessibilitySemanticsSnippets.java[tag=accessibility-semantics-button,indent=0]
@@ -24,7 +24,7 @@ include::../demos/common/src/main/java/com/codenameone/developerguide/snippets/A
Tri-state controls use `UNCHECKED`, `CHECKED`, or `MIXED`. Nullable Boolean properties distinguish an explicit state from an unspecified state. Available properties include selected, expanded, enabled, invalid, busy, read-only, required, multiline, obscured, pressed, and current. `setValidationError()` supplies both the error message and invalid state. `setPaneTitle()` describes a newly displayed pane or screen, and `setModal(true)` identifies a modal scope.
-Use `setRoleDescription()` only when the platform role is not sufficiently specific. Localize labels, hints, descriptions, errors, pane titles, role descriptions, and custom action labels.
+Use `setRoleDescription()` only when the platform role isn't sufficiently specific. Localize labels, hints, descriptions, errors, pane titles, role descriptions, and custom action labels.
== Ranges and editable values
@@ -58,7 +58,7 @@ Use the standard IDs in `AccessibilityAction` for activation, long press, increm
[source,java]
include::../demos/common/src/main/java/com/codenameone/developerguide/snippets/AccessibilitySemanticsSnippets.java[tag=accessibility-semantics-grouping,indent=0]
-For content that is painted without child components, attach an `AccessibilityChildProvider`, or add standalone `AccessibilityNode` children. Every virtual node must use a stable `virtualKey`. Stable keys preserve native focus while rows are updated or recycled.
+For content that's painted without child components, attach an `AccessibilityChildProvider`, or add standalone `AccessibilityNode` children. Every virtual node must use a stable `virtualKey`. Stable keys preserve native focus while rows are updated or recycled.
[source,java]
include::../demos/common/src/main/java/com/codenameone/developerguide/snippets/AccessibilitySemanticsSnippets.java[tag=accessibility-semantics-virtual,indent=0]
@@ -81,7 +81,7 @@ Set `AccessibilityLiveRegion.POLITE` for non-urgent updates and `ASSERTIVE` for
[source,java]
include::../demos/common/src/main/java/com/codenameone/developerguide/snippets/AccessibilitySemanticsSnippets.java[tag=accessibility-semantics-live,indent=0]
-Form changes and nodes with a pane title produce screen or pane transition notifications. `Display.announceForAccessibility()` remains available for an exceptional announcement that is not represented by persistent UI. Prefer a live region for state that is visible on screen.
+Form changes and nodes with a pane title produce screen or pane transition notifications. `Display.announceForAccessibility()` remains available for an exceptional announcement that isn't represented by persistent UI. Prefer a live region for state that's visible on screen.
== Collections
diff --git a/docs/developer-guide/languagetool-accept.txt b/docs/developer-guide/languagetool-accept.txt
index d9445e2bef1..10c8fbc45c9 100644
--- a/docs/developer-guide/languagetool-accept.txt
+++ b/docs/developer-guide/languagetool-accept.txt
@@ -604,6 +604,9 @@ bitmask
DeX
Siri
devirtualization
+# Coordinate variable names used in accessibility hit-testing examples.
+screenX
+screenY
# AR / VR terms (Augmented-Reality.asciidoc, Virtual-Reality.asciidoc)
raycasts?
equirectangular
From 0141d34fcffbdbed86bb203811d8587045f2cf0f Mon Sep 17 00:00:00 2001
From: Shai Almog <67850168+shai-almog@users.noreply.github.com>
Date: Sun, 12 Jul 2026 09:33:39 +0300
Subject: [PATCH 06/15] Satisfy accessibility static analysis
---
CodenameOne/src/com/codename1/ui/Button.java | 4 +-
.../src/com/codename1/ui/CheckBox.java | 3 +-
CodenameOne/src/com/codename1/ui/Display.java | 6 +-
CodenameOne/src/com/codename1/ui/Label.java | 3 +-
CodenameOne/src/com/codename1/ui/List.java | 4 +-
.../src/com/codename1/ui/RadioButton.java | 3 +-
CodenameOne/src/com/codename1/ui/Slider.java | 3 +-
CodenameOne/src/com/codename1/ui/Tabs.java | 4 +-
.../src/com/codename1/ui/TextArea.java | 4 +-
.../ui/accessibility/AccessibilityAction.java | 12 +-
.../AccessibilityAssertions.java | 36 +-
.../AccessibilityCollectionItemInfo.java | 34 +-
.../ui/accessibility/AccessibilityIssue.java | 16 +-
.../accessibility/AccessibilityManager.java | 367 ++++++++++++------
.../ui/accessibility/AccessibilityNode.java | 343 ++++++++++++----
.../AccessibilityNodeSnapshot.java | 152 ++++++--
.../AccessibilityTreeSnapshot.java | 157 +++++---
17 files changed, 823 insertions(+), 328 deletions(-)
diff --git a/CodenameOne/src/com/codename1/ui/Button.java b/CodenameOne/src/com/codename1/ui/Button.java
index 33b0f255c62..0246355d122 100644
--- a/CodenameOne/src/com/codename1/ui/Button.java
+++ b/CodenameOne/src/com/codename1/ui/Button.java
@@ -24,6 +24,7 @@
package com.codename1.ui;
+import com.codename1.ui.accessibility.AccessibilityManager;
import com.codename1.ui.events.ActionEvent;
import com.codename1.ui.events.ActionListener;
import com.codename1.ui.events.ActionSource;
@@ -996,8 +997,7 @@ public void setToggle(boolean toggle) {
return;
}
this.toggle = toggle;
- accessibilityChanged(com.codename1.ui.accessibility.AccessibilityManager.CHANGE_STRUCTURE
- | com.codename1.ui.accessibility.AccessibilityManager.CHANGE_STATE);
+ accessibilityChanged(AccessibilityManager.CHANGE_STRUCTURE | AccessibilityManager.CHANGE_STATE);
if (toggle && "CheckBox".equals(getUIID()) || "RadioButton".equals(getUIID())) {
setUIID("ToggleButton");
}
diff --git a/CodenameOne/src/com/codename1/ui/CheckBox.java b/CodenameOne/src/com/codename1/ui/CheckBox.java
index 90c2cd94e46..4de2a09f6a4 100644
--- a/CodenameOne/src/com/codename1/ui/CheckBox.java
+++ b/CodenameOne/src/com/codename1/ui/CheckBox.java
@@ -24,6 +24,7 @@
package com.codename1.ui;
+import com.codename1.ui.accessibility.AccessibilityManager;
import com.codename1.cloud.BindTarget;
import com.codename1.ui.events.ActionEvent;
import com.codename1.ui.events.ActionListener;
@@ -173,7 +174,7 @@ public void setSelected(boolean selected) {
this.selected = selected;
if (changed) {
fireChangeEvent();
- accessibilityChanged(com.codename1.ui.accessibility.AccessibilityManager.CHANGE_STATE);
+ accessibilityChanged(AccessibilityManager.CHANGE_STATE);
}
repaint();
}
diff --git a/CodenameOne/src/com/codename1/ui/Display.java b/CodenameOne/src/com/codename1/ui/Display.java
index 8cdce2cf3ec..bbcde6c12f6 100644
--- a/CodenameOne/src/com/codename1/ui/Display.java
+++ b/CodenameOne/src/com/codename1/ui/Display.java
@@ -59,6 +59,7 @@
import com.codename1.plugin.event.IsGalleryTypeSupportedEvent;
import com.codename1.plugin.event.OpenGalleryEvent;
import com.codename1.system.CrashReport;
+import com.codename1.ui.accessibility.AccessibilityManager;
import com.codename1.ui.animations.Animation;
import com.codename1.ui.animations.CommonTransitions;
import com.codename1.ui.animations.Transition;
@@ -1746,9 +1747,8 @@ void setCurrentForm(Form newForm) {
lastKeyPressed = 0;
previousKeyPressed = 0;
newForm.onShowCompletedImpl();
- com.codename1.ui.accessibility.AccessibilityManager.getInstance().invalidate(
- newForm, com.codename1.ui.accessibility.AccessibilityManager.CHANGE_STRUCTURE
- | com.codename1.ui.accessibility.AccessibilityManager.CHANGE_PANE);
+ AccessibilityManager.getInstance().invalidate(newForm,
+ AccessibilityManager.CHANGE_STRUCTURE | AccessibilityManager.CHANGE_PANE);
}
private boolean applyInitialWindowSize(Form form) {
diff --git a/CodenameOne/src/com/codename1/ui/Label.java b/CodenameOne/src/com/codename1/ui/Label.java
index 72e6e84cfe2..0e08ace3df5 100644
--- a/CodenameOne/src/com/codename1/ui/Label.java
+++ b/CodenameOne/src/com/codename1/ui/Label.java
@@ -23,6 +23,7 @@
*/
package com.codename1.ui;
+import com.codename1.ui.accessibility.AccessibilityManager;
import com.codename1.cloud.BindTarget;
import com.codename1.io.Log;
import com.codename1.ui.TextSelection.Char;
@@ -646,7 +647,7 @@ public void setText(String text) {
stringWidthUnselected = -1;
setShouldCalcPreferredSize(true);
if (oldText == null ? this.text != null : !oldText.equals(this.text)) {
- accessibilityChanged(com.codename1.ui.accessibility.AccessibilityManager.CHANGE_CONTENT);
+ accessibilityChanged(AccessibilityManager.CHANGE_CONTENT);
}
repaint();
}
diff --git a/CodenameOne/src/com/codename1/ui/List.java b/CodenameOne/src/com/codename1/ui/List.java
index 5980b92ca6c..b1687a01c99 100644
--- a/CodenameOne/src/com/codename1/ui/List.java
+++ b/CodenameOne/src/com/codename1/ui/List.java
@@ -23,6 +23,7 @@
*/
package com.codename1.ui;
+import com.codename1.ui.accessibility.AccessibilityManager;
import com.codename1.compat.java.util.Objects;
import com.codename1.ui.animations.Motion;
import com.codename1.ui.events.ActionEvent;
@@ -549,8 +550,7 @@ public void setSelectedIndex(int index, boolean scrollToSelection) {
int oldIndex = model.getSelectedIndex();
model.setSelectedIndex(index);
if (oldIndex != index) {
- accessibilityChanged(com.codename1.ui.accessibility.AccessibilityManager.CHANGE_STATE
- | com.codename1.ui.accessibility.AccessibilityManager.CHANGE_VALUE);
+ accessibilityChanged(AccessibilityManager.CHANGE_STATE | AccessibilityManager.CHANGE_VALUE);
}
if (!isInitialized()) {
Form f = getComponentForm();
diff --git a/CodenameOne/src/com/codename1/ui/RadioButton.java b/CodenameOne/src/com/codename1/ui/RadioButton.java
index 07cb36793ed..6cf49607fd6 100644
--- a/CodenameOne/src/com/codename1/ui/RadioButton.java
+++ b/CodenameOne/src/com/codename1/ui/RadioButton.java
@@ -23,6 +23,7 @@
*/
package com.codename1.ui;
+import com.codename1.ui.accessibility.AccessibilityManager;
import com.codename1.cloud.BindTarget;
import com.codename1.ui.events.ActionEvent;
import com.codename1.ui.events.ActionListener;
@@ -255,7 +256,7 @@ void setSelectedImpl(boolean selected) {
this.selected = selected;
if (changed) {
fireChangeEvent();
- accessibilityChanged(com.codename1.ui.accessibility.AccessibilityManager.CHANGE_STATE);
+ accessibilityChanged(AccessibilityManager.CHANGE_STATE);
}
repaint();
}
diff --git a/CodenameOne/src/com/codename1/ui/Slider.java b/CodenameOne/src/com/codename1/ui/Slider.java
index 0ff06ff30f5..7ff7ab3435f 100644
--- a/CodenameOne/src/com/codename1/ui/Slider.java
+++ b/CodenameOne/src/com/codename1/ui/Slider.java
@@ -23,6 +23,7 @@
*/
package com.codename1.ui;
+import com.codename1.ui.accessibility.AccessibilityManager;
import com.codename1.ui.events.ActionEvent;
import com.codename1.ui.events.ActionListener;
@@ -307,7 +308,7 @@ protected void setProgressInternal(int value) {
int oldValue = this.value;
this.value = value;
if (oldValue != value) {
- accessibilityChanged(com.codename1.ui.accessibility.AccessibilityManager.CHANGE_VALUE);
+ accessibilityChanged(AccessibilityManager.CHANGE_VALUE);
}
if (renderValueOnTop || renderPercentageOnTop) {
super.setText(formattedValue(value));
diff --git a/CodenameOne/src/com/codename1/ui/Tabs.java b/CodenameOne/src/com/codename1/ui/Tabs.java
index a43c9cf0f9d..d7e4c17c6d8 100644
--- a/CodenameOne/src/com/codename1/ui/Tabs.java
+++ b/CodenameOne/src/com/codename1/ui/Tabs.java
@@ -23,6 +23,7 @@
*/
package com.codename1.ui;
+import com.codename1.ui.accessibility.AccessibilityManager;
import com.codename1.ui.animations.Motion;
import com.codename1.ui.events.ActionEvent;
import com.codename1.ui.events.ActionListener;
@@ -1300,8 +1301,7 @@ public void setSelectedIndex(int index, boolean slideToSelected) {
if (index == activeComponent) {
return;
}
- accessibilityChanged(com.codename1.ui.accessibility.AccessibilityManager.CHANGE_STATE
- | com.codename1.ui.accessibility.AccessibilityManager.CHANGE_PANE);
+ accessibilityChanged(AccessibilityManager.CHANGE_STATE | AccessibilityManager.CHANGE_PANE);
// Snapshot the current tab bounds *before* we mutate state, so the
// animated indicator can tween from where it visibly is to the new
// selection's bounds.
diff --git a/CodenameOne/src/com/codename1/ui/TextArea.java b/CodenameOne/src/com/codename1/ui/TextArea.java
index aea2cdee7b8..261d3773f75 100644
--- a/CodenameOne/src/com/codename1/ui/TextArea.java
+++ b/CodenameOne/src/com/codename1/ui/TextArea.java
@@ -23,6 +23,7 @@
*/
package com.codename1.ui;
+import com.codename1.ui.accessibility.AccessibilityManager;
import com.codename1.cloud.BindTarget;
import com.codename1.compat.java.util.Objects;
import com.codename1.io.Log;
@@ -609,8 +610,7 @@ && textMightGrowByContent(old, text)) {
}
if (!Objects.equals(text, old)) {
fireDataChanged(DataChangedListener.CHANGED, -1);
- accessibilityChanged(com.codename1.ui.accessibility.AccessibilityManager.CHANGE_VALUE
- | com.codename1.ui.accessibility.AccessibilityManager.CHANGE_CONTENT);
+ accessibilityChanged(AccessibilityManager.CHANGE_VALUE | AccessibilityManager.CHANGE_CONTENT);
}
// while native editing we don't need the cursor animations
if (Display.getInstance().isNativeInputSupported() && Display.getInstance().isTextEditing(this)) {
diff --git a/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityAction.java b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityAction.java
index a0bb01d945d..9e6f67a542c 100644
--- a/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityAction.java
+++ b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityAction.java
@@ -53,9 +53,15 @@ public AccessibilityAction(String id, String label, Handler handler, boolean ena
this.enabled = enabled;
}
- public String getId() { return id; }
- public String getLabel() { return label; }
- public boolean isEnabled() { return enabled; }
+ public String getId() {
+ return id;
+ }
+ public String getLabel() {
+ return label;
+ }
+ public boolean isEnabled() {
+ return enabled;
+ }
public boolean perform(Component component, Object argument) {
return enabled && handler != null && handler.perform(component, argument);
diff --git a/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityAssertions.java b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityAssertions.java
index 139dab70908..de532109434 100644
--- a/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityAssertions.java
+++ b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityAssertions.java
@@ -30,23 +30,31 @@ public static void assertNoErrors(AccessibilityTreeSnapshot tree) {
StringBuilder errors = new StringBuilder();
for (AccessibilityIssue issue : issues) {
if (issue.getSeverity() == AccessibilityIssue.Severity.ERROR) {
- if (errors.length() > 0) errors.append('\n');
+ if (errors.length() > 0) {
+ errors.append('\n');
+ }
errors.append(issue.toString());
}
}
- if (errors.length() > 0) throw new AssertionError(errors.toString());
+ if (errors.length() > 0) {
+ throw new AssertionError(errors.toString());
+ }
}
public static void assertNoUnlabeledInteractiveNodes(AccessibilityTreeSnapshot tree) {
StringBuilder errors = new StringBuilder();
for (AccessibilityNodeSnapshot node : tree.getNodes().values()) {
if (node.isFocusable() && empty(node.getLabel()) && empty(node.getValue())) {
- if (errors.length() > 0) errors.append('\n');
+ if (errors.length() > 0) {
+ errors.append('\n');
+ }
errors.append("Interactive node ").append(node.getId()).append(" (")
.append(node.getRole()).append(") has no accessible name");
}
}
- if (errors.length() > 0) throw new AssertionError(errors.toString());
+ if (errors.length() > 0) {
+ throw new AssertionError(errors.toString());
+ }
}
private static void auditNode(AccessibilityTreeSnapshot tree, AccessibilityNodeSnapshot node,
@@ -92,7 +100,9 @@ private static void auditNode(AccessibilityTreeSnapshot tree, AccessibilityNodeS
}
Set actions = new HashSet();
for (AccessibilityAction action : node.getActions()) {
- if (!actions.add(action.getId())) error(issues, "duplicate-action", "Action ids must be unique per node", node);
+ if (!actions.add(action.getId())) {
+ error(issues, "duplicate-action", "Action ids must be unique per node", node);
+ }
if (!isStandardAction(action.getId()) && empty(action.getLabel())) {
error(issues, "unlabeled-custom-action", "Custom accessibility actions require localized labels", node);
}
@@ -106,15 +116,21 @@ && empty(node.getLabel()) && empty(node.getPaneTitle())) {
private static void detectCycle(AccessibilityTreeSnapshot tree, long id, Set visiting,
Set visited, List issues) {
Long boxed = Long.valueOf(id);
- if (visited.contains(boxed)) return;
+ if (visited.contains(boxed)) {
+ return;
+ }
if (!visiting.add(boxed)) {
AccessibilityNodeSnapshot node = tree.getNode(id);
- if (node != null) error(issues, "tree-cycle", "Semantic tree contains a cycle", node);
+ if (node != null) {
+ error(issues, "tree-cycle", "Semantic tree contains a cycle", node);
+ }
return;
}
AccessibilityNodeSnapshot node = tree.getNode(id);
if (node != null) {
- for (Long child : node.getChildIds()) detectCycle(tree, child.longValue(), visiting, visited, issues);
+ for (Long child : node.getChildIds()) {
+ detectCycle(tree, child.longValue(), visiting, visited, issues);
+ }
}
visiting.remove(boxed);
visited.add(boxed);
@@ -146,5 +162,7 @@ private static void warning(List issues, String code, String
issues.add(new AccessibilityIssue(AccessibilityIssue.Severity.WARNING, code, message, node.getId()));
}
- private static boolean empty(String value) { return value == null || value.length() == 0; }
+ private static boolean empty(String value) {
+ return value == null || value.length() == 0;
+ }
}
diff --git a/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityCollectionItemInfo.java b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityCollectionItemInfo.java
index 0ea48ccfbbc..a511633e7cb 100644
--- a/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityCollectionItemInfo.java
+++ b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityCollectionItemInfo.java
@@ -19,7 +19,7 @@ public AccessibilityCollectionItemInfo(int rowIndex, int columnIndex) {
}
public AccessibilityCollectionItemInfo(int rowIndex, int rowSpan, int columnIndex, int columnSpan,
- int positionInSet, int setSize, int level, boolean heading) {
+ int positionInSet, int setSize, int level, boolean heading) {
this.rowIndex = rowIndex;
this.rowSpan = rowSpan;
this.columnIndex = columnIndex;
@@ -30,12 +30,28 @@ public AccessibilityCollectionItemInfo(int rowIndex, int rowSpan, int columnInde
this.heading = heading;
}
- public int getRowIndex() { return rowIndex; }
- public int getRowSpan() { return rowSpan; }
- public int getColumnIndex() { return columnIndex; }
- public int getColumnSpan() { return columnSpan; }
- public int getPositionInSet() { return positionInSet; }
- public int getSetSize() { return setSize; }
- public int getLevel() { return level; }
- public boolean isHeading() { return heading; }
+ public int getRowIndex() {
+ return rowIndex;
+ }
+ public int getRowSpan() {
+ return rowSpan;
+ }
+ public int getColumnIndex() {
+ return columnIndex;
+ }
+ public int getColumnSpan() {
+ return columnSpan;
+ }
+ public int getPositionInSet() {
+ return positionInSet;
+ }
+ public int getSetSize() {
+ return setSize;
+ }
+ public int getLevel() {
+ return level;
+ }
+ public boolean isHeading() {
+ return heading;
+ }
}
diff --git a/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityIssue.java b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityIssue.java
index 6251e00703b..d80a0e764c9 100644
--- a/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityIssue.java
+++ b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityIssue.java
@@ -19,10 +19,18 @@ public AccessibilityIssue(Severity severity, String code, String message, long n
this.nodeId = nodeId;
}
- public Severity getSeverity() { return severity; }
- public String getCode() { return code; }
- public String getMessage() { return message; }
- public long getNodeId() { return nodeId; }
+ public Severity getSeverity() {
+ return severity;
+ }
+ public String getCode() {
+ return code;
+ }
+ public String getMessage() {
+ return message;
+ }
+ public long getNodeId() {
+ return nodeId;
+ }
@Override
public String toString() {
diff --git a/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityManager.java b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityManager.java
index 33ccb4d715d..88d78e3a236 100644
--- a/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityManager.java
+++ b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityManager.java
@@ -53,8 +53,8 @@ public final class AccessibilityManager {
private boolean refreshScheduled;
private int pendingChanges = CHANGE_ALL;
private Form snapshotForm;
- private AccessibilityTreeSnapshot snapshot = new AccessibilityTreeSnapshot(0,
- Collections.emptyList(), Collections.emptyMap());
+ private AccessibilityTreeSnapshot snapshot = new AccessibilityTreeSnapshot(
+ 0, Collections.emptyList(), Collections.emptyMap());
private AccessibilityManager() {
}
@@ -70,6 +70,7 @@ public synchronized void invalidate(Component component, int changeType) {
if (!refreshScheduled) {
refreshScheduled = true;
Display.getInstance().callSerially(new Runnable() {
+ @Override
public void run() {
int changes;
synchronized (AccessibilityManager.this) {
@@ -95,16 +96,21 @@ public void invalidateAll() {
public AccessibilityTreeSnapshot getCurrentSnapshot() {
synchronized (this) {
- if (dirty && !Display.getInstance().isEdt()) return snapshot;
+ if (dirty && !Display.getInstance().isEdt()) {
+ return snapshot;
+ }
}
return getSnapshot(Display.getInstance().getCurrent());
}
+ @SuppressWarnings("PMD.CompareObjectsWithEquals")
public synchronized AccessibilityTreeSnapshot getSnapshot(Form form) {
- if (!dirty && form == snapshotForm) return snapshot;
+ if (!dirty && form == snapshotForm) {
+ return snapshot;
+ }
if (form == null) {
snapshot = new AccessibilityTreeSnapshot(++generation, Collections.emptyList(),
- Collections.emptyMap());
+ Collections.emptyMap());
snapshotForm = null;
dirty = false;
pendingChanges = 0;
@@ -135,8 +141,11 @@ public boolean performAction(long nodeId, String actionId, final Object argument
node = snapshot.getNode(nodeId);
action = node == null ? null : node.getAction(actionId);
}
- if (node == null || action == null || !action.isEnabled()) return false;
+ if (node == null || action == null || !action.isEnabled()) {
+ return false;
+ }
Display.getInstance().callSerially(new Runnable() {
+ @Override
public void run() {
action.perform(node.getComponent(), argument);
invalidate(node.getComponent(), CHANGE_STATE | CHANGE_VALUE | CHANGE_CONTENT);
@@ -151,7 +160,9 @@ public boolean performActionByHash(long nodeId, int actionIdHash, Object argumen
synchronized (this) {
node = snapshot.getNode(nodeId);
}
- if (node == null) return false;
+ if (node == null) {
+ return false;
+ }
for (AccessibilityAction action : node.getActions()) {
if (action.getId().hashCode() == actionIdHash) {
return performAction(nodeId, action.getId(), argument);
@@ -180,37 +191,47 @@ private long idForVirtual(Component host, String path) {
}
private void resolveComponent(Component component, List destination) {
- if (component == null || (!(component instanceof Form) && !component.isVisible())
- || component.isHidden(true)) return;
+ if (component == null || (!(component instanceof Form) && !component.isVisible()) || component.isHidden(true)) {
+ return;
+ }
AccessibilityNode config = component.getSemantics();
AccessibilityGrouping grouping = config.getGrouping();
- if (grouping == AccessibilityGrouping.EXCLUDE_SUBTREE) return;
+ if (grouping == AccessibilityGrouping.EXCLUDE_SUBTREE) {
+ return;
+ }
BuildNode node = buildComponentNode(component, config);
List children = new ArrayList();
if (grouping != AccessibilityGrouping.LEAF && component instanceof Container) {
- Container container = (Container)component;
+ Container container = (Container) component;
for (int i = 0; i < container.getComponentCount(); i++) {
resolveComponent(container.getComponentAt(i), children);
}
}
if (grouping != AccessibilityGrouping.LEAF) {
- addVirtualChildren(component, config, node.id, "custom", children);
+ addVirtualChildren(component, config, "custom", children);
if (component instanceof com.codename1.ui.List) {
- addListChildren((com.codename1.ui.List)component, node.id, children);
+ addListChildren((com.codename1.ui.List) component, children);
}
}
boolean expose = shouldExpose(component, config, node);
- if (grouping == AccessibilityGrouping.EXCLUDE) expose = false;
+ if (grouping == AccessibilityGrouping.EXCLUDE) {
+ expose = false;
+ }
if (grouping == AccessibilityGrouping.MERGE_DESCENDANTS) {
String merged = collectLabels(children);
- if (isEmpty(node.builder.label)) node.builder.label = merged;
- else if (!isEmpty(merged)) node.builder.description = join(node.builder.description, merged);
+ if (isEmpty(node.builder.label)) {
+ node.builder.label = merged;
+ } else if (!isEmpty(merged)) {
+ node.builder.description = join(node.builder.description, merged);
+ }
children.clear();
expose = true;
}
- if (grouping == AccessibilityGrouping.LEAF || grouping == AccessibilityGrouping.GROUP) expose = true;
+ if (grouping == AccessibilityGrouping.LEAF || grouping == AccessibilityGrouping.GROUP) {
+ expose = true;
+ }
if (expose) {
node.children.addAll(children);
@@ -237,14 +258,18 @@ private BuildNode buildComponentNode(final Component component, AccessibilityNod
out.builder.checked = inferChecked(component, config);
out.builder.liveRegion = config.getLiveRegion();
out.builder.range = config.getRange() == null ? inferRange(component) : config.getRange();
- out.builder.collectionInfo = config.getCollectionInfo() == null ? inferCollection(component) : config.getCollectionInfo();
- out.builder.collectionItemInfo = config.getCollectionItemInfo() == null
- ? inferCollectionItem(component) : config.getCollectionItemInfo();
+ out.builder.collectionInfo =
+ config.getCollectionInfo() == null ? inferCollection(component) : config.getCollectionInfo();
+ out.builder.collectionItemInfo = config.getCollectionItemInfo() == null ? inferCollectionItem(component)
+ : config.getCollectionItemInfo();
out.builder.bounds = componentBounds(component);
out.builder.selected = config.getSelected();
- if (out.builder.selected == null) applyInferredSelected(component, out.builder);
+ if (out.builder.selected == null) {
+ applyInferredSelected(component, out.builder);
+ }
out.builder.expanded = config.getExpanded();
- out.builder.enabled = config.getEnabled() == null ? Boolean.valueOf(component.isEnabled()) : config.getEnabled();
+ out.builder.enabled =
+ config.getEnabled() == null ? Boolean.valueOf(component.isEnabled()) : config.getEnabled();
out.builder.invalid = config.getInvalid();
out.builder.busy = config.getBusy();
out.builder.readOnly = config.getReadOnly();
@@ -264,26 +289,30 @@ private BuildNode buildComponentNode(final Component component, AccessibilityNod
out.builder.actions.addAll(config.getActions());
addDefaultActions(component, out.builder);
if (component instanceof Form && isEmpty(out.builder.paneTitle)) {
- out.builder.paneTitle = ((Form)component).getTitle();
+ out.builder.paneTitle = ((Form) component).getTitle();
}
out.component = component;
return out;
}
- private void addVirtualChildren(Component host, AccessibilityNode config, long hostId,
- String path, List destination) {
+ private void addVirtualChildren(Component host, AccessibilityNode config, String path,
+ List destination) {
List virtual = new ArrayList();
virtual.addAll(config.getChildren());
if (config.getChildProvider() != null) {
List provided = config.getChildProvider().getAccessibilityChildren(host);
- if (provided != null) virtual.addAll(provided);
+ if (provided != null) {
+ virtual.addAll(provided);
+ }
}
for (int i = 0; i < virtual.size(); i++) {
AccessibilityNode child = virtual.get(i);
String key = child.getVirtualKey();
- if (isEmpty(key)) key = "index-" + i;
+ if (isEmpty(key)) {
+ key = "index-" + i;
+ }
BuildNode resolved = buildVirtualNode(host, child, path + "/" + key);
- addVirtualChildren(host, child, resolved.id, path + "/" + key, resolved.children);
+ addVirtualChildren(host, child, path + "/" + key, resolved.children);
destination.add(resolved);
}
}
@@ -329,158 +358,198 @@ private BuildNode buildVirtualNode(Component host, AccessibilityNode config, Str
return out;
}
- private void addListChildren(final com.codename1.ui.List list, long hostId, List destination) {
+ private void addListChildren(final com.codename1.ui.List list, List destination) {
int size = list.size();
for (int i = 0; i < size; i++) {
final int index = i;
AccessibilityNode item = new AccessibilityNode("item-" + i)
- .setRole(AccessibilityRole.LIST_ITEM)
- .setLabel(list.getAccessibilityItemText(i))
- .setSelected(Boolean.valueOf(i == list.getSelectedIndex()))
- .setCollectionItemInfo(new AccessibilityCollectionItemInfo(i, 1, 0, 1,
- i + 1, size, 1, false))
- .setBounds(list.getAccessibilityItemBounds(i, new Rectangle()))
- .addAction(new AccessibilityAction(AccessibilityAction.ACTIVATE, null,
- new ListActivateHandler(list, index)));
+ .setRole(AccessibilityRole.LIST_ITEM)
+ .setLabel(list.getAccessibilityItemText(i))
+ .setSelected(Boolean.valueOf(i == list.getSelectedIndex()))
+ .setCollectionItemInfo(new AccessibilityCollectionItemInfo(
+ i, 1, 0, 1, i + 1, size, 1, false))
+ .setBounds(list.getAccessibilityItemBounds(i, new Rectangle()))
+ .addAction(new AccessibilityAction(AccessibilityAction.ACTIVATE, null,
+ new ListActivateHandler(list, index)));
destination.add(buildVirtualNode(list, item, "list/item-" + i));
}
}
private void addDefaultActions(final Component component, AccessibilityNodeSnapshot.Builder builder) {
if (component.isFocusable() && !hasAction(builder.actions, AccessibilityAction.FOCUS)) {
- builder.actions.add(new AccessibilityAction(AccessibilityAction.FOCUS, null,
- FocusHandler.INSTANCE));
+ builder.actions.add(new AccessibilityAction(AccessibilityAction.FOCUS, null, FocusHandler.INSTANCE));
}
if (component instanceof Button && !hasAction(builder.actions, AccessibilityAction.ACTIVATE)) {
- builder.actions.add(new AccessibilityAction(AccessibilityAction.ACTIVATE, null,
- ActivateHandler.INSTANCE));
+ builder.actions.add(new AccessibilityAction(AccessibilityAction.ACTIVATE, null, ActivateHandler.INSTANCE));
}
if (component instanceof Slider) {
- final Slider slider = (Slider)component;
+ final Slider slider = (Slider) component;
if (slider.isEditable() && !hasAction(builder.actions, AccessibilityAction.INCREMENT)) {
builder.actions.add(new AccessibilityAction(AccessibilityAction.INCREMENT, null,
- new SliderAdjustmentHandler(slider, 1)));
+ new SliderAdjustmentHandler(slider, 1)));
}
if (slider.isEditable() && !hasAction(builder.actions, AccessibilityAction.DECREMENT)) {
builder.actions.add(new AccessibilityAction(AccessibilityAction.DECREMENT, null,
- new SliderAdjustmentHandler(slider, -1)));
+ new SliderAdjustmentHandler(slider, -1)));
}
}
if (component instanceof TextArea) {
- final TextArea text = (TextArea)component;
+ final TextArea text = (TextArea) component;
if (!hasAction(builder.actions, AccessibilityAction.FOCUS)) {
- builder.actions.add(new AccessibilityAction(AccessibilityAction.FOCUS, null,
- FocusHandler.INSTANCE));
+ builder.actions.add(new AccessibilityAction(AccessibilityAction.FOCUS, null, FocusHandler.INSTANCE));
}
if (text.isEditable() && !hasAction(builder.actions, AccessibilityAction.SET_TEXT)) {
- builder.actions.add(new AccessibilityAction(AccessibilityAction.SET_TEXT, null,
- new SetTextHandler(text)));
+ builder.actions.add(
+ new AccessibilityAction(AccessibilityAction.SET_TEXT, null, new SetTextHandler(text)));
}
}
}
private AccessibilityRole inferRole(Component component) {
if (component.getParent() instanceof Table) {
- return ((Table)component.getParent()).getCellRow(component) < 0
- ? AccessibilityRole.COLUMN_HEADER : AccessibilityRole.CELL;
+ return ((Table) component.getParent()).getCellRow(component) < 0 ? AccessibilityRole.COLUMN_HEADER
+ : AccessibilityRole.CELL;
}
Tabs tabOwner = tabPanelOwner(component);
- if (tabOwner != null) return AccessibilityRole.TAB_PANEL;
- if (component instanceof Dialog) return AccessibilityRole.DIALOG;
- if (component instanceof RadioButton) return AccessibilityRole.RADIO_BUTTON;
- if (component instanceof CheckBox) return AccessibilityRole.CHECKBOX;
+ if (tabOwner != null) {
+ return AccessibilityRole.TAB_PANEL;
+ }
+ if (component instanceof Dialog) {
+ return AccessibilityRole.DIALOG;
+ }
+ if (component instanceof RadioButton) {
+ return AccessibilityRole.RADIO_BUTTON;
+ }
+ if (component instanceof CheckBox) {
+ return AccessibilityRole.CHECKBOX;
+ }
if (component instanceof Button) {
- if (isTabButton((Button)component)) return AccessibilityRole.TAB;
- return ((Button)component).isToggle() ? AccessibilityRole.TOGGLE_BUTTON : AccessibilityRole.BUTTON;
- }
- if (component instanceof Slider) return ((Slider)component).isEditable() ? AccessibilityRole.SLIDER : AccessibilityRole.PROGRESS_BAR;
- if (component instanceof TextField) return AccessibilityRole.TEXT_FIELD;
- if (component instanceof TextArea) return AccessibilityRole.TEXT_FIELD;
- if (component instanceof com.codename1.ui.List) return AccessibilityRole.LIST;
- if (component instanceof Table) return AccessibilityRole.GRID;
- if (component instanceof Tabs) return AccessibilityRole.TAB_LIST;
- if (component instanceof Form) return AccessibilityRole.GENERIC;
- if (component instanceof Label) return AccessibilityRole.STATIC_TEXT;
+ if (isTabButton((Button) component)) {
+ return AccessibilityRole.TAB;
+ }
+ return ((Button) component).isToggle() ? AccessibilityRole.TOGGLE_BUTTON : AccessibilityRole.BUTTON;
+ }
+ if (component instanceof Slider) {
+ return ((Slider) component).isEditable() ? AccessibilityRole.SLIDER : AccessibilityRole.PROGRESS_BAR;
+ }
+ if (component instanceof TextField) {
+ return AccessibilityRole.TEXT_FIELD;
+ }
+ if (component instanceof TextArea) {
+ return AccessibilityRole.TEXT_FIELD;
+ }
+ if (component instanceof com.codename1.ui.List) {
+ return AccessibilityRole.LIST;
+ }
+ if (component instanceof Table) {
+ return AccessibilityRole.GRID;
+ }
+ if (component instanceof Tabs) {
+ return AccessibilityRole.TAB_LIST;
+ }
+ if (component instanceof Form) {
+ return AccessibilityRole.GENERIC;
+ }
+ if (component instanceof Label) {
+ return AccessibilityRole.STATIC_TEXT;
+ }
return AccessibilityRole.NONE;
}
+ @SuppressWarnings("PMD.CompareObjectsWithEquals")
private boolean isTabButton(Button button) {
Container parent = button.getParent();
while (parent != null) {
Container owner = parent.getParent();
- if (owner instanceof Tabs && ((Tabs)owner).getTabsContainer() == parent) return true;
+ if (owner instanceof Tabs && ((Tabs) owner).getTabsContainer() == parent) {
+ return true;
+ }
parent = parent.getParent();
}
return false;
}
private AccessibilityCheckedState inferChecked(Component component, AccessibilityNode config) {
- if (config.getChecked() != AccessibilityCheckedState.UNSPECIFIED) return config.getChecked();
+ if (config.getChecked() != AccessibilityCheckedState.UNSPECIFIED) {
+ return config.getChecked();
+ }
if (component instanceof CheckBox || component instanceof RadioButton) {
- return ((Button)component).isSelected() ? AccessibilityCheckedState.CHECKED : AccessibilityCheckedState.UNCHECKED;
+ return ((Button) component).isSelected() ? AccessibilityCheckedState.CHECKED
+ : AccessibilityCheckedState.UNCHECKED;
}
return AccessibilityCheckedState.UNSPECIFIED;
}
+ @SuppressWarnings("PMD.CompareObjectsWithEquals")
private void applyInferredSelected(Component component, AccessibilityNodeSnapshot.Builder builder) {
- if (component instanceof Button && (((Button)component).isToggle() || isTabButton((Button)component))) {
- builder.selected = Boolean.valueOf(((Button)component).isSelected());
+ if (component instanceof Button && (((Button) component).isToggle() || isTabButton((Button) component))) {
+ builder.selected = Boolean.valueOf(((Button) component).isSelected());
return;
}
Tabs tabOwner = tabPanelOwner(component);
- if (tabOwner != null) builder.selected = Boolean.valueOf(tabOwner.getSelectedComponent() == component);
+ if (tabOwner != null) {
+ builder.selected = Boolean.valueOf(tabOwner.getSelectedComponent() == component);
+ }
}
+ @SuppressWarnings("PMD.CompareObjectsWithEquals")
private Tabs tabPanelOwner(Component component) {
Container parent = component.getParent();
if (parent != null && parent.getParent() instanceof Tabs) {
- Tabs tabs = (Tabs)parent.getParent();
- if (tabs.getContentPane() == parent) return tabs;
+ Tabs tabs = (Tabs) parent.getParent();
+ if (tabs.getContentPane() == parent) {
+ return tabs;
+ }
}
return null;
}
private AccessibilityRange inferRange(Component component) {
if (component instanceof Slider) {
- Slider slider = (Slider)component;
- return new AccessibilityRange(slider.getMinValue(), slider.getMaxValue(), slider.getProgress(), slider.getIncrements(), null);
+ Slider slider = (Slider) component;
+ return new AccessibilityRange(slider.getMinValue(), slider.getMaxValue(), slider.getProgress(),
+ slider.getIncrements(), null);
}
return null;
}
private AccessibilityCollectionInfo inferCollection(Component component) {
if (component instanceof com.codename1.ui.List) {
- return new AccessibilityCollectionInfo(((com.codename1.ui.List)component).size(), 1,
- false, AccessibilityCollectionInfo.SELECTION_SINGLE);
+ return new AccessibilityCollectionInfo(((com.codename1.ui.List) component).size(), 1, false,
+ AccessibilityCollectionInfo.SELECTION_SINGLE);
}
if (component instanceof Table) {
- Table table = (Table)component;
+ Table table = (Table) component;
return new AccessibilityCollectionInfo(table.getModel().getRowCount(), table.getModel().getColumnCount(),
- false, AccessibilityCollectionInfo.SELECTION_SINGLE);
+ false, AccessibilityCollectionInfo.SELECTION_SINGLE);
}
if (component instanceof Tabs) {
- return new AccessibilityCollectionInfo(1, ((Tabs)component).getTabCount(), false,
- AccessibilityCollectionInfo.SELECTION_SINGLE);
+ return new AccessibilityCollectionInfo(1, ((Tabs) component).getTabCount(), false,
+ AccessibilityCollectionInfo.SELECTION_SINGLE);
}
return null;
}
private AccessibilityCollectionItemInfo inferCollectionItem(Component component) {
if (component.getParent() instanceof Table) {
- Table table = (Table)component.getParent();
+ Table table = (Table) component.getParent();
int sourceRow = table.getCellRow(component);
int row = sourceRow < 0 ? 0 : sourceRow + (table.isIncludeHeader() ? 1 : 0);
int column = table.getCellColumn(component);
- return new AccessibilityCollectionItemInfo(row, 1, column, 1, column + 1,
- table.getModel().getColumnCount(), 1, sourceRow < 0);
+ return new AccessibilityCollectionItemInfo(row, 1, column, 1, column + 1, table.getModel().getColumnCount(),
+ 1, sourceRow < 0);
}
return null;
}
private void applyInferredTextStates(Component component, AccessibilityNodeSnapshot.Builder builder) {
- if (!(component instanceof TextArea)) return;
- TextArea text = (TextArea)component;
- if (builder.readOnly == null) builder.readOnly = Boolean.valueOf(!text.isEditable());
+ if (!(component instanceof TextArea)) {
+ return;
+ }
+ TextArea text = (TextArea) component;
+ if (builder.readOnly == null) {
+ builder.readOnly = Boolean.valueOf(!text.isEditable());
+ }
if (builder.multiline == null) {
builder.multiline = Boolean.valueOf(!(component instanceof TextField) || text.getRows() > 1);
}
@@ -490,20 +559,26 @@ private void applyInferredTextStates(Component component, AccessibilityNodeSnaps
}
private boolean shouldExpose(Component component, AccessibilityNode config, BuildNode node) {
- if (component instanceof Form) return true;
- if (config.hasExplicitConfiguration()) return true;
- return node.builder.role != AccessibilityRole.NONE && (!isEmpty(node.builder.label)
- || !isEmpty(node.builder.value) || node.builder.focusable || !node.builder.actions.isEmpty()
- || node.builder.collectionInfo != null || node.builder.range != null);
+ if (component instanceof Form) {
+ return true;
+ }
+ if (config.hasExplicitConfiguration()) {
+ return true;
+ }
+ return node.builder.role != AccessibilityRole.NONE &&
+ (!isEmpty(node.builder.label) || !isEmpty(node.builder.value) || node.builder.focusable ||
+ !node.builder.actions.isEmpty() || node.builder.collectionInfo != null || node.builder.range != null);
}
private Rectangle componentBounds(Component component) {
Rectangle bounds = new Rectangle(component.getAbsoluteX() + component.getScrollX(),
- component.getAbsoluteY() + component.getScrollY(), component.getWidth(), component.getHeight());
+ component.getAbsoluteY() + component.getScrollY(), component.getWidth(),
+ component.getHeight());
Container parent = component.getParent();
while (parent != null && bounds.getWidth() > 0 && bounds.getHeight() > 0) {
- Rectangle clip = new Rectangle(parent.getAbsoluteX() + parent.getScrollX(),
- parent.getAbsoluteY() + parent.getScrollY(), parent.getWidth(), parent.getHeight());
+ Rectangle clip =
+ new Rectangle(parent.getAbsoluteX() + parent.getScrollX(),
+ parent.getAbsoluteY() + parent.getScrollY(), parent.getWidth(), parent.getHeight());
intersect(bounds, clip);
parent = parent.getParent();
}
@@ -511,9 +586,12 @@ private Rectangle componentBounds(Component component) {
}
private Rectangle virtualBounds(Component host, Rectangle relative) {
- if (relative == null) return componentBounds(host);
+ if (relative == null) {
+ return componentBounds(host);
+ }
Rectangle bounds = new Rectangle(host.getAbsoluteX() + host.getScrollX() + relative.getX(),
- host.getAbsoluteY() + host.getScrollY() + relative.getY(), relative.getWidth(), relative.getHeight());
+ host.getAbsoluteY() + host.getScrollY() + relative.getY(), relative.getWidth(),
+ relative.getHeight());
intersect(bounds, componentBounds(host));
return bounds;
}
@@ -529,11 +607,14 @@ private void intersect(Rectangle target, Rectangle clip) {
private void sortTree(List nodes) {
Collections.sort(nodes, SortKeyComparator.INSTANCE);
applyRelativeOrder(nodes);
- for (BuildNode node : nodes) sortTree(node.children);
+ for (BuildNode node : nodes) {
+ sortTree(node.children);
+ }
}
private void applyRelativeOrder(List nodes) {
- for (int pass = 0; pass < nodes.size(); pass++) {
+ int remainingPasses = nodes.size();
+ while (remainingPasses > 0) {
boolean changed = false;
for (int i = 0; i < nodes.size(); i++) {
BuildNode node = nodes.get(i);
@@ -552,21 +633,33 @@ private void applyRelativeOrder(List nodes) {
break;
}
}
- if (!changed) return;
+ if (!changed) {
+ return;
+ }
+ remainingPasses--;
}
}
+ @SuppressWarnings("PMD.CompareObjectsWithEquals")
private int indexOf(List nodes, Component component) {
- if (component == null) return -1;
- for (int i = 0; i < nodes.size(); i++) if (nodes.get(i).component == component) return i;
+ if (component == null) {
+ return -1;
+ }
+ for (int i = 0; i < nodes.size(); i++) {
+ if (nodes.get(i).component == component) {
+ return i;
+ }
+ }
return -1;
}
private void freeze(List source, long parentId, List childIds,
- LinkedHashMap nodes) {
+ LinkedHashMap nodes) {
for (BuildNode node : source) {
node.builder.parentId = parentId;
- for (BuildNode child : node.children) node.builder.childIds.add(Long.valueOf(child.id));
+ for (BuildNode child : node.children) {
+ node.builder.childIds.add(Long.valueOf(child.id));
+ }
AccessibilityNodeSnapshot frozen = new AccessibilityNodeSnapshot(node.builder);
nodes.put(Long.valueOf(node.id), frozen);
childIds.add(Long.valueOf(node.id));
@@ -585,18 +678,22 @@ private String collectLabels(List nodes) {
}
private static boolean hasAction(List actions, String id) {
- for (AccessibilityAction action : actions) if (id.equals(action.getId())) return true;
+ for (AccessibilityAction action : actions) {
+ if (id.equals(action.getId())) {
+ return true;
+ }
+ }
return false;
}
private static boolean isInteractiveRole(AccessibilityRole role) {
- return role == AccessibilityRole.BUTTON || role == AccessibilityRole.TOGGLE_BUTTON
- || role == AccessibilityRole.CHECKBOX || role == AccessibilityRole.RADIO_BUTTON
- || role == AccessibilityRole.SWITCH || role == AccessibilityRole.LINK
- || role == AccessibilityRole.TEXT_FIELD || role == AccessibilityRole.SEARCH_FIELD
- || role == AccessibilityRole.SLIDER || role == AccessibilityRole.TAB
- || role == AccessibilityRole.MENU_ITEM || role == AccessibilityRole.SPIN_BUTTON
- || role == AccessibilityRole.COMBO_BOX || role == AccessibilityRole.TREE_ITEM;
+ return role == AccessibilityRole.BUTTON || role == AccessibilityRole.TOGGLE_BUTTON ||
+ role == AccessibilityRole.CHECKBOX || role == AccessibilityRole.RADIO_BUTTON ||
+ role == AccessibilityRole.SWITCH || role == AccessibilityRole.LINK ||
+ role == AccessibilityRole.TEXT_FIELD || role == AccessibilityRole.SEARCH_FIELD ||
+ role == AccessibilityRole.SLIDER || role == AccessibilityRole.TAB ||
+ role == AccessibilityRole.MENU_ITEM || role == AccessibilityRole.SPIN_BUTTON ||
+ role == AccessibilityRole.COMBO_BOX || role == AccessibilityRole.TREE_ITEM;
}
private static String firstNonEmpty(String first, String second) {
@@ -604,8 +701,12 @@ private static String firstNonEmpty(String first, String second) {
}
private static String join(String first, String second) {
- if (isEmpty(first)) return second;
- if (isEmpty(second)) return first;
+ if (isEmpty(first)) {
+ return second;
+ }
+ if (isEmpty(second)) {
+ return first;
+ }
return first + ", " + second;
}
@@ -623,12 +724,19 @@ private static final class BuildNode {
private static final class SortKeyComparator implements Comparator {
private static final SortKeyComparator INSTANCE = new SortKeyComparator();
+ @Override
public int compare(BuildNode a, BuildNode b) {
boolean an = Double.isNaN(a.builder.sortKey);
boolean bn = Double.isNaN(b.builder.sortKey);
- if (an && bn) return 0;
- if (an) return 1;
- if (bn) return -1;
+ if (an && bn) {
+ return 0;
+ }
+ if (an) {
+ return 1;
+ }
+ if (bn) {
+ return -1;
+ }
return Double.compare(a.builder.sortKey, b.builder.sortKey);
}
}
@@ -636,8 +744,11 @@ public int compare(BuildNode a, BuildNode b) {
private static final class FocusHandler implements AccessibilityAction.Handler {
private static final FocusHandler INSTANCE = new FocusHandler();
+ @Override
public boolean perform(Component source, Object argument) {
- if (!source.isEnabled()) return false;
+ if (!source.isEnabled()) {
+ return false;
+ }
source.requestFocus();
return true;
}
@@ -646,8 +757,11 @@ public boolean perform(Component source, Object argument) {
private static final class ActivateHandler implements AccessibilityAction.Handler {
private static final ActivateHandler INSTANCE = new ActivateHandler();
+ @Override
public boolean perform(Component source, Object argument) {
- if (!source.isEnabled()) return false;
+ if (!source.isEnabled()) {
+ return false;
+ }
source.keyReleased(Display.getInstance().getKeyCode(Display.GAME_FIRE));
return true;
}
@@ -662,8 +776,11 @@ private ListActivateHandler(com.codename1.ui.List list, int index) {
this.index = index;
}
+ @Override
public boolean perform(Component source, Object argument) {
- if (!list.isEnabled()) return false;
+ if (!list.isEnabled()) {
+ return false;
+ }
list.setSelectedIndex(index);
list.keyReleased(Display.getInstance().getKeyCode(Display.GAME_FIRE));
return true;
@@ -679,6 +796,7 @@ private SliderAdjustmentHandler(Slider slider, int direction) {
this.direction = direction;
}
+ @Override
public boolean perform(Component source, Object argument) {
int increment = Math.max(1, slider.getIncrements());
int value = slider.getProgress() + direction * increment;
@@ -694,9 +812,12 @@ private SetTextHandler(TextArea text) {
this.text = text;
}
+ @Override
public boolean perform(Component source, Object argument) {
- if (!(argument instanceof String)) return false;
- text.setText((String)argument);
+ if (!(argument instanceof String)) {
+ return false;
+ }
+ text.setText((String) argument);
return true;
}
}
diff --git a/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityNode.java b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityNode.java
index 282fa251282..d3ba2e2e132 100644
--- a/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityNode.java
+++ b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityNode.java
@@ -72,8 +72,12 @@ private AccessibilityNode changed(int type) {
return this;
}
- public Component getOwner() { return owner; }
- public String getVirtualKey() { return virtualKey; }
+ public Component getOwner() {
+ return owner;
+ }
+ public String getVirtualKey() {
+ return virtualKey;
+ }
public AccessibilityNode setVirtualKey(String virtualKey) {
if (virtualKey == null || virtualKey.length() == 0) {
throw new IllegalArgumentException("Virtual accessibility nodes require a non-empty stable key");
@@ -81,77 +85,250 @@ public AccessibilityNode setVirtualKey(String virtualKey) {
this.virtualKey = virtualKey;
return changed(AccessibilityManager.CHANGE_STRUCTURE);
}
- public String getIdentifier() { return identifier; }
- public AccessibilityNode setIdentifier(String identifier) { this.identifier = identifier; return changed(AccessibilityManager.CHANGE_CONTENT); }
- public String getLabel() { return label; }
- public AccessibilityNode setLabel(String label) { this.label = label; return changed(AccessibilityManager.CHANGE_CONTENT); }
- public String getHint() { return hint; }
- public AccessibilityNode setHint(String hint) { this.hint = hint; return changed(AccessibilityManager.CHANGE_CONTENT); }
- public String getDescription() { return description; }
- public AccessibilityNode setDescription(String description) { this.description = description; return changed(AccessibilityManager.CHANGE_CONTENT); }
- public String getValue() { return value; }
- public AccessibilityNode setValue(String value) { this.value = value; return changed(AccessibilityManager.CHANGE_VALUE); }
- public String getValidationError() { return validationError; }
- public AccessibilityNode setValidationError(String error) { this.validationError = error; this.invalid = error == null ? invalid : Boolean.TRUE; return changed(AccessibilityManager.CHANGE_STATE); }
- public String getPaneTitle() { return paneTitle; }
- public AccessibilityNode setPaneTitle(String paneTitle) { this.paneTitle = paneTitle; return changed(AccessibilityManager.CHANGE_PANE); }
- public String getRoleDescription() { return roleDescription; }
- public AccessibilityNode setRoleDescription(String roleDescription) { this.roleDescription = roleDescription; return changed(AccessibilityManager.CHANGE_CONTENT); }
- public AccessibilityRole getRole() { return role; }
- public AccessibilityNode setRole(AccessibilityRole role) { this.role = role == null ? AccessibilityRole.NONE : role; return changed(AccessibilityManager.CHANGE_STRUCTURE); }
- public AccessibilityCheckedState getChecked() { return checked; }
- public AccessibilityNode setChecked(AccessibilityCheckedState checked) { this.checked = checked == null ? AccessibilityCheckedState.UNSPECIFIED : checked; return changed(AccessibilityManager.CHANGE_STATE); }
- public AccessibilityLiveRegion getLiveRegion() { return liveRegion; }
- public AccessibilityNode setLiveRegion(AccessibilityLiveRegion liveRegion) { this.liveRegion = liveRegion == null ? AccessibilityLiveRegion.OFF : liveRegion; return changed(AccessibilityManager.CHANGE_LIVE_REGION); }
- public AccessibilityGrouping getGrouping() { return grouping; }
- public AccessibilityNode setGrouping(AccessibilityGrouping grouping) { this.grouping = grouping == null ? AccessibilityGrouping.AUTO : grouping; return changed(AccessibilityManager.CHANGE_STRUCTURE); }
- public AccessibilityRange getRange() { return range; }
- public AccessibilityNode setRange(AccessibilityRange range) { this.range = range; return changed(AccessibilityManager.CHANGE_VALUE); }
- public AccessibilityCollectionInfo getCollectionInfo() { return collectionInfo; }
- public AccessibilityNode setCollectionInfo(AccessibilityCollectionInfo info) { this.collectionInfo = info; return changed(AccessibilityManager.CHANGE_STRUCTURE); }
- public AccessibilityCollectionItemInfo getCollectionItemInfo() { return collectionItemInfo; }
- public AccessibilityNode setCollectionItemInfo(AccessibilityCollectionItemInfo info) { this.collectionItemInfo = info; return changed(AccessibilityManager.CHANGE_STRUCTURE); }
- public AccessibilityChildProvider getChildProvider() { return childProvider; }
- public AccessibilityNode setChildProvider(AccessibilityChildProvider provider) { this.childProvider = provider; return changed(AccessibilityManager.CHANGE_STRUCTURE); }
- public Rectangle getBounds() { return bounds == null ? null : new Rectangle(bounds); }
- public AccessibilityNode setBounds(Rectangle bounds) { this.bounds = bounds == null ? null : new Rectangle(bounds); return changed(AccessibilityManager.CHANGE_BOUNDS); }
- public Boolean getSelected() { return selected; }
- public AccessibilityNode setSelected(Boolean selected) { this.selected = selected; return changed(AccessibilityManager.CHANGE_STATE); }
- public Boolean getExpanded() { return expanded; }
- public AccessibilityNode setExpanded(Boolean expanded) { this.expanded = expanded; return changed(AccessibilityManager.CHANGE_STATE); }
- public Boolean getEnabled() { return enabled; }
- public AccessibilityNode setEnabled(Boolean enabled) { this.enabled = enabled; return changed(AccessibilityManager.CHANGE_STATE); }
- public Boolean getInvalid() { return invalid; }
- public AccessibilityNode setInvalid(Boolean invalid) { this.invalid = invalid; return changed(AccessibilityManager.CHANGE_STATE); }
- public Boolean getBusy() { return busy; }
- public AccessibilityNode setBusy(Boolean busy) { this.busy = busy; return changed(AccessibilityManager.CHANGE_STATE); }
- public Boolean getReadOnly() { return readOnly; }
- public AccessibilityNode setReadOnly(Boolean readOnly) { this.readOnly = readOnly; return changed(AccessibilityManager.CHANGE_STATE); }
- public Boolean getRequired() { return required; }
- public AccessibilityNode setRequired(Boolean required) { this.required = required; return changed(AccessibilityManager.CHANGE_STATE); }
- public Boolean getMultiline() { return multiline; }
- public AccessibilityNode setMultiline(Boolean multiline) { this.multiline = multiline; return changed(AccessibilityManager.CHANGE_STATE); }
- public Boolean getObscured() { return obscured; }
- public AccessibilityNode setObscured(Boolean obscured) { this.obscured = obscured; return changed(AccessibilityManager.CHANGE_STATE); }
- public Boolean getPressed() { return pressed; }
- public AccessibilityNode setPressed(Boolean pressed) { this.pressed = pressed; return changed(AccessibilityManager.CHANGE_STATE); }
- public Boolean getCurrent() { return current; }
- public AccessibilityNode setCurrent(Boolean current) { this.current = current; return changed(AccessibilityManager.CHANGE_STATE); }
- public boolean isModal() { return modal; }
- public AccessibilityNode setModal(boolean modal) { this.modal = modal; return changed(AccessibilityManager.CHANGE_PANE); }
- public int getHeadingLevel() { return headingLevel; }
- public AccessibilityNode setHeadingLevel(int level) { this.headingLevel = level; if (level > 0 && role == AccessibilityRole.NONE) role = AccessibilityRole.HEADING; return changed(AccessibilityManager.CHANGE_STRUCTURE); }
- public double getSortKey() { return sortKey; }
- public AccessibilityNode setSortKey(double sortKey) { this.sortKey = sortKey; return changed(AccessibilityManager.CHANGE_STRUCTURE); }
- public Component getTraversalBefore() { return traversalBefore; }
- public AccessibilityNode setTraversalBefore(Component component) { this.traversalBefore = component; return changed(AccessibilityManager.CHANGE_STRUCTURE); }
- public Component getTraversalAfter() { return traversalAfter; }
- public AccessibilityNode setTraversalAfter(Component component) { this.traversalAfter = component; return changed(AccessibilityManager.CHANGE_STRUCTURE); }
+ public String getIdentifier() {
+ return identifier;
+ }
+ public AccessibilityNode setIdentifier(String identifier) {
+ this.identifier = identifier;
+ return changed(AccessibilityManager.CHANGE_CONTENT);
+ }
+ public String getLabel() {
+ return label;
+ }
+ public AccessibilityNode setLabel(String label) {
+ this.label = label;
+ return changed(AccessibilityManager.CHANGE_CONTENT);
+ }
+ public String getHint() {
+ return hint;
+ }
+ public AccessibilityNode setHint(String hint) {
+ this.hint = hint;
+ return changed(AccessibilityManager.CHANGE_CONTENT);
+ }
+ public String getDescription() {
+ return description;
+ }
+ public AccessibilityNode setDescription(String description) {
+ this.description = description;
+ return changed(AccessibilityManager.CHANGE_CONTENT);
+ }
+ public String getValue() {
+ return value;
+ }
+ public AccessibilityNode setValue(String value) {
+ this.value = value;
+ return changed(AccessibilityManager.CHANGE_VALUE);
+ }
+ public String getValidationError() {
+ return validationError;
+ }
+ public AccessibilityNode setValidationError(String error) {
+ this.validationError = error;
+ this.invalid = error == null ? invalid : Boolean.TRUE;
+ return changed(AccessibilityManager.CHANGE_STATE);
+ }
+ public String getPaneTitle() {
+ return paneTitle;
+ }
+ public AccessibilityNode setPaneTitle(String paneTitle) {
+ this.paneTitle = paneTitle;
+ return changed(AccessibilityManager.CHANGE_PANE);
+ }
+ public String getRoleDescription() {
+ return roleDescription;
+ }
+ public AccessibilityNode setRoleDescription(String roleDescription) {
+ this.roleDescription = roleDescription;
+ return changed(AccessibilityManager.CHANGE_CONTENT);
+ }
+ public AccessibilityRole getRole() {
+ return role;
+ }
+ public AccessibilityNode setRole(AccessibilityRole role) {
+ this.role = role == null ? AccessibilityRole.NONE : role;
+ return changed(AccessibilityManager.CHANGE_STRUCTURE);
+ }
+ public AccessibilityCheckedState getChecked() {
+ return checked;
+ }
+ public AccessibilityNode setChecked(AccessibilityCheckedState checked) {
+ this.checked = checked == null ? AccessibilityCheckedState.UNSPECIFIED : checked;
+ return changed(AccessibilityManager.CHANGE_STATE);
+ }
+ public AccessibilityLiveRegion getLiveRegion() {
+ return liveRegion;
+ }
+ public AccessibilityNode setLiveRegion(AccessibilityLiveRegion liveRegion) {
+ this.liveRegion = liveRegion == null ? AccessibilityLiveRegion.OFF : liveRegion;
+ return changed(AccessibilityManager.CHANGE_LIVE_REGION);
+ }
+ public AccessibilityGrouping getGrouping() {
+ return grouping;
+ }
+ public AccessibilityNode setGrouping(AccessibilityGrouping grouping) {
+ this.grouping = grouping == null ? AccessibilityGrouping.AUTO : grouping;
+ return changed(AccessibilityManager.CHANGE_STRUCTURE);
+ }
+ public AccessibilityRange getRange() {
+ return range;
+ }
+ public AccessibilityNode setRange(AccessibilityRange range) {
+ this.range = range;
+ return changed(AccessibilityManager.CHANGE_VALUE);
+ }
+ public AccessibilityCollectionInfo getCollectionInfo() {
+ return collectionInfo;
+ }
+ public AccessibilityNode setCollectionInfo(AccessibilityCollectionInfo info) {
+ this.collectionInfo = info;
+ return changed(AccessibilityManager.CHANGE_STRUCTURE);
+ }
+ public AccessibilityCollectionItemInfo getCollectionItemInfo() {
+ return collectionItemInfo;
+ }
+ public AccessibilityNode setCollectionItemInfo(AccessibilityCollectionItemInfo info) {
+ this.collectionItemInfo = info;
+ return changed(AccessibilityManager.CHANGE_STRUCTURE);
+ }
+ public AccessibilityChildProvider getChildProvider() {
+ return childProvider;
+ }
+ public AccessibilityNode setChildProvider(AccessibilityChildProvider provider) {
+ this.childProvider = provider;
+ return changed(AccessibilityManager.CHANGE_STRUCTURE);
+ }
+ public Rectangle getBounds() {
+ return bounds == null ? null : new Rectangle(bounds);
+ }
+ public AccessibilityNode setBounds(Rectangle bounds) {
+ this.bounds = bounds == null ? null : new Rectangle(bounds);
+ return changed(AccessibilityManager.CHANGE_BOUNDS);
+ }
+ public Boolean getSelected() {
+ return selected;
+ }
+ public AccessibilityNode setSelected(Boolean selected) {
+ this.selected = selected;
+ return changed(AccessibilityManager.CHANGE_STATE);
+ }
+ public Boolean getExpanded() {
+ return expanded;
+ }
+ public AccessibilityNode setExpanded(Boolean expanded) {
+ this.expanded = expanded;
+ return changed(AccessibilityManager.CHANGE_STATE);
+ }
+ public Boolean getEnabled() {
+ return enabled;
+ }
+ public AccessibilityNode setEnabled(Boolean enabled) {
+ this.enabled = enabled;
+ return changed(AccessibilityManager.CHANGE_STATE);
+ }
+ public Boolean getInvalid() {
+ return invalid;
+ }
+ public AccessibilityNode setInvalid(Boolean invalid) {
+ this.invalid = invalid;
+ return changed(AccessibilityManager.CHANGE_STATE);
+ }
+ public Boolean getBusy() {
+ return busy;
+ }
+ public AccessibilityNode setBusy(Boolean busy) {
+ this.busy = busy;
+ return changed(AccessibilityManager.CHANGE_STATE);
+ }
+ public Boolean getReadOnly() {
+ return readOnly;
+ }
+ public AccessibilityNode setReadOnly(Boolean readOnly) {
+ this.readOnly = readOnly;
+ return changed(AccessibilityManager.CHANGE_STATE);
+ }
+ public Boolean getRequired() {
+ return required;
+ }
+ public AccessibilityNode setRequired(Boolean required) {
+ this.required = required;
+ return changed(AccessibilityManager.CHANGE_STATE);
+ }
+ public Boolean getMultiline() {
+ return multiline;
+ }
+ public AccessibilityNode setMultiline(Boolean multiline) {
+ this.multiline = multiline;
+ return changed(AccessibilityManager.CHANGE_STATE);
+ }
+ public Boolean getObscured() {
+ return obscured;
+ }
+ public AccessibilityNode setObscured(Boolean obscured) {
+ this.obscured = obscured;
+ return changed(AccessibilityManager.CHANGE_STATE);
+ }
+ public Boolean getPressed() {
+ return pressed;
+ }
+ public AccessibilityNode setPressed(Boolean pressed) {
+ this.pressed = pressed;
+ return changed(AccessibilityManager.CHANGE_STATE);
+ }
+ public Boolean getCurrent() {
+ return current;
+ }
+ public AccessibilityNode setCurrent(Boolean current) {
+ this.current = current;
+ return changed(AccessibilityManager.CHANGE_STATE);
+ }
+ public boolean isModal() {
+ return modal;
+ }
+ public AccessibilityNode setModal(boolean modal) {
+ this.modal = modal;
+ return changed(AccessibilityManager.CHANGE_PANE);
+ }
+ public int getHeadingLevel() {
+ return headingLevel;
+ }
+ public AccessibilityNode setHeadingLevel(int level) {
+ this.headingLevel = level;
+ if (level > 0 && role == AccessibilityRole.NONE) {
+ role = AccessibilityRole.HEADING;
+ }
+ return changed(AccessibilityManager.CHANGE_STRUCTURE);
+ }
+ public double getSortKey() {
+ return sortKey;
+ }
+ public AccessibilityNode setSortKey(double sortKey) {
+ this.sortKey = sortKey;
+ return changed(AccessibilityManager.CHANGE_STRUCTURE);
+ }
+ public Component getTraversalBefore() {
+ return traversalBefore;
+ }
+ public AccessibilityNode setTraversalBefore(Component component) {
+ this.traversalBefore = component;
+ return changed(AccessibilityManager.CHANGE_STRUCTURE);
+ }
+ public Component getTraversalAfter() {
+ return traversalAfter;
+ }
+ public AccessibilityNode setTraversalAfter(Component component) {
+ this.traversalAfter = component;
+ return changed(AccessibilityManager.CHANGE_STRUCTURE);
+ }
public AccessibilityNode addAction(AccessibilityAction action) {
- if (action == null) throw new NullPointerException("action");
+ if (action == null) {
+ throw new NullPointerException("action");
+ }
for (int i = actions.size() - 1; i >= 0; i--) {
- if (actions.get(i).getId().equals(action.getId())) actions.remove(i);
+ if (actions.get(i).getId().equals(action.getId())) {
+ actions.remove(i);
+ }
}
actions.add(action);
return changed(AccessibilityManager.CHANGE_ACTIONS);
@@ -159,7 +336,9 @@ public AccessibilityNode addAction(AccessibilityAction action) {
public AccessibilityNode removeAction(String id) {
for (int i = actions.size() - 1; i >= 0; i--) {
- if (actions.get(i).getId().equals(id)) actions.remove(i);
+ if (actions.get(i).getId().equals(id)) {
+ actions.remove(i);
+ }
}
return changed(AccessibilityManager.CHANGE_ACTIONS);
}
@@ -169,7 +348,9 @@ public List getActions() {
}
public AccessibilityNode addChild(AccessibilityNode child) {
- if (child == null) throw new NullPointerException("child");
+ if (child == null) {
+ throw new NullPointerException("child");
+ }
children.add(child);
return changed(AccessibilityManager.CHANGE_STRUCTURE);
}
@@ -184,14 +365,14 @@ public List getChildren() {
}
public boolean hasExplicitConfiguration() {
- return role != AccessibilityRole.NONE || label != null || hint != null || description != null
- || value != null || validationError != null || paneTitle != null || roleDescription != null
- || checked != AccessibilityCheckedState.UNSPECIFIED || liveRegion != AccessibilityLiveRegion.OFF
- || grouping != AccessibilityGrouping.AUTO || range != null || collectionInfo != null
- || collectionItemInfo != null || childProvider != null || bounds != null || selected != null
- || expanded != null || enabled != null || invalid != null || busy != null || readOnly != null
- || required != null || multiline != null || obscured != null || pressed != null || current != null
- || modal || headingLevel > 0 || !Double.isNaN(sortKey) || traversalBefore != null
- || traversalAfter != null || !actions.isEmpty() || !children.isEmpty();
+ return role != AccessibilityRole.NONE || label != null || hint != null || description != null ||
+ value != null || validationError != null || paneTitle != null || roleDescription != null ||
+ checked != AccessibilityCheckedState.UNSPECIFIED || liveRegion != AccessibilityLiveRegion.OFF ||
+ grouping != AccessibilityGrouping.AUTO || range != null || collectionInfo != null ||
+ collectionItemInfo != null || childProvider != null || bounds != null || selected != null ||
+ expanded != null || enabled != null || invalid != null || busy != null || readOnly != null ||
+ required != null || multiline != null || obscured != null || pressed != null || current != null ||
+ modal || headingLevel > 0 || !Double.isNaN(sortKey) || traversalBefore != null ||
+ traversalAfter != null || !actions.isEmpty() || !children.isEmpty();
}
}
diff --git a/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityNodeSnapshot.java b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityNodeSnapshot.java
index a60f35a51e4..0b05e63f4b9 100644
--- a/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityNodeSnapshot.java
+++ b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityNodeSnapshot.java
@@ -87,47 +87,123 @@ public final class AccessibilityNodeSnapshot {
actions = Collections.unmodifiableList(new ArrayList(b.actions));
}
- public long getId() { return id; }
- public long getParentId() { return parentId; }
- public Component getComponent() { return component; }
- public String getVirtualKey() { return virtualKey; }
- public String getIdentifier() { return identifier; }
- public String getLabel() { return label; }
- public String getHint() { return hint; }
- public String getDescription() { return description; }
- public String getValue() { return value; }
- public String getValidationError() { return validationError; }
- public String getPaneTitle() { return paneTitle; }
- public String getRoleDescription() { return roleDescription; }
- public AccessibilityRole getRole() { return role; }
- public AccessibilityCheckedState getChecked() { return checked; }
- public AccessibilityLiveRegion getLiveRegion() { return liveRegion; }
- public AccessibilityRange getRange() { return range; }
- public AccessibilityCollectionInfo getCollectionInfo() { return collectionInfo; }
- public AccessibilityCollectionItemInfo getCollectionItemInfo() { return collectionItemInfo; }
- public Rectangle getBounds() { return new Rectangle(bounds); }
- public Boolean getSelected() { return selected; }
- public Boolean getExpanded() { return expanded; }
- public Boolean getEnabled() { return enabled; }
- public Boolean getInvalid() { return invalid; }
- public Boolean getBusy() { return busy; }
- public Boolean getReadOnly() { return readOnly; }
- public Boolean getRequired() { return required; }
- public Boolean getMultiline() { return multiline; }
- public Boolean getObscured() { return obscured; }
- public Boolean getPressed() { return pressed; }
- public Boolean getCurrent() { return current; }
- public boolean isModal() { return modal; }
- public boolean isFocusable() { return focusable; }
- public boolean isFocused() { return focused; }
- public int getHeadingLevel() { return headingLevel; }
- public List getChildIds() { return childIds; }
- public List getActions() { return actions; }
+ public long getId() {
+ return id;
+ }
+ public long getParentId() {
+ return parentId;
+ }
+ public Component getComponent() {
+ return component;
+ }
+ public String getVirtualKey() {
+ return virtualKey;
+ }
+ public String getIdentifier() {
+ return identifier;
+ }
+ public String getLabel() {
+ return label;
+ }
+ public String getHint() {
+ return hint;
+ }
+ public String getDescription() {
+ return description;
+ }
+ public String getValue() {
+ return value;
+ }
+ public String getValidationError() {
+ return validationError;
+ }
+ public String getPaneTitle() {
+ return paneTitle;
+ }
+ public String getRoleDescription() {
+ return roleDescription;
+ }
+ public AccessibilityRole getRole() {
+ return role;
+ }
+ public AccessibilityCheckedState getChecked() {
+ return checked;
+ }
+ public AccessibilityLiveRegion getLiveRegion() {
+ return liveRegion;
+ }
+ public AccessibilityRange getRange() {
+ return range;
+ }
+ public AccessibilityCollectionInfo getCollectionInfo() {
+ return collectionInfo;
+ }
+ public AccessibilityCollectionItemInfo getCollectionItemInfo() {
+ return collectionItemInfo;
+ }
+ public Rectangle getBounds() {
+ return new Rectangle(bounds);
+ }
+ public Boolean getSelected() {
+ return selected;
+ }
+ public Boolean getExpanded() {
+ return expanded;
+ }
+ public Boolean getEnabled() {
+ return enabled;
+ }
+ public Boolean getInvalid() {
+ return invalid;
+ }
+ public Boolean getBusy() {
+ return busy;
+ }
+ public Boolean getReadOnly() {
+ return readOnly;
+ }
+ public Boolean getRequired() {
+ return required;
+ }
+ public Boolean getMultiline() {
+ return multiline;
+ }
+ public Boolean getObscured() {
+ return obscured;
+ }
+ public Boolean getPressed() {
+ return pressed;
+ }
+ public Boolean getCurrent() {
+ return current;
+ }
+ public boolean isModal() {
+ return modal;
+ }
+ public boolean isFocusable() {
+ return focusable;
+ }
+ public boolean isFocused() {
+ return focused;
+ }
+ public int getHeadingLevel() {
+ return headingLevel;
+ }
+ public List getChildIds() {
+ return childIds;
+ }
+ public List getActions() {
+ return actions;
+ }
public AccessibilityAction getAction(String id) {
- if (id == null) return null;
+ if (id == null) {
+ return null;
+ }
for (AccessibilityAction action : actions) {
- if (id.equals(action.getId())) return action;
+ if (id.equals(action.getId())) {
+ return action;
+ }
}
return null;
}
diff --git a/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityTreeSnapshot.java b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityTreeSnapshot.java
index a252a59cf16..14595131d7c 100644
--- a/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityTreeSnapshot.java
+++ b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityTreeSnapshot.java
@@ -22,10 +22,18 @@ public final class AccessibilityTreeSnapshot {
this.nodes = Collections.unmodifiableMap(new LinkedHashMap(nodes));
}
- public long getGeneration() { return generation; }
- public List getRootIds() { return rootIds; }
- public Map getNodes() { return nodes; }
- public AccessibilityNodeSnapshot getNode(long id) { return nodes.get(Long.valueOf(id)); }
+ public long getGeneration() {
+ return generation;
+ }
+ public List getRootIds() {
+ return rootIds;
+ }
+ public Map getNodes() {
+ return nodes;
+ }
+ public AccessibilityNodeSnapshot getNode(long id) {
+ return nodes.get(Long.valueOf(id));
+ }
/// Returns the deepest semantic node containing the screen coordinate.
public AccessibilityNodeSnapshot getNodeAt(int x, int y) {
@@ -33,7 +41,9 @@ public AccessibilityNodeSnapshot getNodeAt(int x, int y) {
for (AccessibilityNodeSnapshot node : nodes.values()) {
Rectangle r = node.getBounds();
if (r.getWidth() > 0 && r.getHeight() > 0 && r.contains(x, y)) {
- if (best == null || isDescendant(node, best)) best = node;
+ if (best == null || isDescendant(node, best)) {
+ best = node;
+ }
}
}
return best;
@@ -42,9 +52,13 @@ public AccessibilityNodeSnapshot getNodeAt(int x, int y) {
private boolean isDescendant(AccessibilityNodeSnapshot possibleChild, AccessibilityNodeSnapshot possibleParent) {
long parent = possibleChild.getParentId();
while (parent >= 0) {
- if (parent == possibleParent.getId()) return true;
+ if (parent == possibleParent.getId()) {
+ return true;
+ }
AccessibilityNodeSnapshot node = getNode(parent);
- if (node == null) return false;
+ if (node == null) {
+ return false;
+ }
parent = node.getParentId();
}
return false;
@@ -54,38 +68,65 @@ public String toJson() {
StringBuilder out = new StringBuilder();
out.append("{\"generation\":").append(generation).append(",\"roots\":[");
for (int i = 0; i < rootIds.size(); i++) {
- if (i > 0) out.append(',');
+ if (i > 0) {
+ out.append(',');
+ }
out.append(rootIds.get(i));
}
out.append("],\"nodes\":[");
- int index = 0;
+ boolean firstNode = true;
for (AccessibilityNodeSnapshot node : nodes.values()) {
- if (index++ > 0) out.append(',');
+ if (!firstNode) {
+ out.append(',');
+ }
+ firstNode = false;
out.append('{');
out.append("\"id\":").append(node.getId());
out.append(",\"parent\":").append(node.getParentId());
- out.append(",\"role\":"); appendJson(out, node.getRole().name());
- out.append(",\"label\":"); appendJson(out, node.getLabel());
- out.append(",\"value\":"); appendJson(out, node.getValue());
- out.append(",\"hint\":"); appendJson(out, node.getHint());
- out.append(",\"description\":"); appendJson(out, node.getDescription());
- out.append(",\"error\":"); appendJson(out, node.getValidationError());
- out.append(",\"paneTitle\":"); appendJson(out, node.getPaneTitle());
- out.append(",\"identifier\":"); appendJson(out, node.getIdentifier());
- out.append(",\"roleDescription\":"); appendJson(out, node.getRoleDescription());
- out.append(",\"checked\":"); appendJson(out, node.getChecked().name());
- out.append(",\"liveRegion\":"); appendJson(out, node.getLiveRegion().name());
- out.append(",\"selected\":"); appendBoolean(out, node.getSelected());
- out.append(",\"expanded\":"); appendBoolean(out, node.getExpanded());
- out.append(",\"enabled\":"); appendBoolean(out, node.getEnabled());
- out.append(",\"invalid\":"); appendBoolean(out, node.getInvalid());
- out.append(",\"busy\":"); appendBoolean(out, node.getBusy());
- out.append(",\"readOnly\":"); appendBoolean(out, node.getReadOnly());
- out.append(",\"required\":"); appendBoolean(out, node.getRequired());
- out.append(",\"multiline\":"); appendBoolean(out, node.getMultiline());
- out.append(",\"obscured\":"); appendBoolean(out, node.getObscured());
- out.append(",\"pressed\":"); appendBoolean(out, node.getPressed());
- out.append(",\"current\":"); appendBoolean(out, node.getCurrent());
+ out.append(",\"role\":");
+ appendJson(out, node.getRole().name());
+ out.append(",\"label\":");
+ appendJson(out, node.getLabel());
+ out.append(",\"value\":");
+ appendJson(out, node.getValue());
+ out.append(",\"hint\":");
+ appendJson(out, node.getHint());
+ out.append(",\"description\":");
+ appendJson(out, node.getDescription());
+ out.append(",\"error\":");
+ appendJson(out, node.getValidationError());
+ out.append(",\"paneTitle\":");
+ appendJson(out, node.getPaneTitle());
+ out.append(",\"identifier\":");
+ appendJson(out, node.getIdentifier());
+ out.append(",\"roleDescription\":");
+ appendJson(out, node.getRoleDescription());
+ out.append(",\"checked\":");
+ appendJson(out, node.getChecked().name());
+ out.append(",\"liveRegion\":");
+ appendJson(out, node.getLiveRegion().name());
+ out.append(",\"selected\":");
+ appendBoolean(out, node.getSelected());
+ out.append(",\"expanded\":");
+ appendBoolean(out, node.getExpanded());
+ out.append(",\"enabled\":");
+ appendBoolean(out, node.getEnabled());
+ out.append(",\"invalid\":");
+ appendBoolean(out, node.getInvalid());
+ out.append(",\"busy\":");
+ appendBoolean(out, node.getBusy());
+ out.append(",\"readOnly\":");
+ appendBoolean(out, node.getReadOnly());
+ out.append(",\"required\":");
+ appendBoolean(out, node.getRequired());
+ out.append(",\"multiline\":");
+ appendBoolean(out, node.getMultiline());
+ out.append(",\"obscured\":");
+ appendBoolean(out, node.getObscured());
+ out.append(",\"pressed\":");
+ appendBoolean(out, node.getPressed());
+ out.append(",\"current\":");
+ appendBoolean(out, node.getCurrent());
out.append(",\"modal\":").append(node.isModal());
out.append(",\"focusable\":").append(node.isFocusable());
out.append(",\"focused\":").append(node.isFocused());
@@ -97,7 +138,8 @@ public String toJson() {
out.append(",\"max\":").append(range.getMaximum());
out.append(",\"current\":").append(range.getCurrent());
out.append(",\"step\":").append(range.getStep());
- out.append(",\"text\":"); appendJson(out, range.getText());
+ out.append(",\"text\":");
+ appendJson(out, range.getText());
out.append('}');
}
if (node.getCollectionInfo() != null) {
@@ -121,19 +163,32 @@ public String toJson() {
out.append(",\"heading\":").append(item.isHeading()).append('}');
}
Rectangle bounds = node.getBounds();
- out.append(",\"bounds\":[").append(bounds.getX()).append(',').append(bounds.getY())
- .append(',').append(bounds.getWidth()).append(',').append(bounds.getHeight()).append(']');
+ out.append(",\"bounds\":[")
+ .append(bounds.getX())
+ .append(',')
+ .append(bounds.getY())
+ .append(',')
+ .append(bounds.getWidth())
+ .append(',')
+ .append(bounds.getHeight())
+ .append(']');
out.append(",\"children\":[");
for (int i = 0; i < node.getChildIds().size(); i++) {
- if (i > 0) out.append(',');
+ if (i > 0) {
+ out.append(',');
+ }
out.append(node.getChildIds().get(i));
}
out.append("],\"actions\":[");
for (int i = 0; i < node.getActions().size(); i++) {
- if (i > 0) out.append(',');
+ if (i > 0) {
+ out.append(',');
+ }
AccessibilityAction action = node.getActions().get(i);
- out.append('{').append("\"id\":"); appendJson(out, action.getId());
- out.append(",\"label\":"); appendJson(out, action.getLabel());
+ out.append('{').append("\"id\":");
+ appendJson(out, action.getId());
+ out.append(",\"label\":");
+ appendJson(out, action.getLabel());
out.append(",\"enabled\":").append(action.isEnabled()).append('}');
}
out.append("]}");
@@ -150,17 +205,27 @@ private static void appendJson(StringBuilder out, String value) {
out.append('"');
for (int i = 0; i < value.length(); i++) {
char c = value.charAt(i);
- if (c == '"' || c == '\\') out.append('\\');
- if (c == '\n') out.append("\\n");
- else if (c == '\r') out.append("\\r");
- else if (c == '\t') out.append("\\t");
- else out.append(c);
+ if (c == '"' || c == '\\') {
+ out.append('\\');
+ }
+ if (c == '\n') {
+ out.append("\\n");
+ } else if (c == '\r') {
+ out.append("\\r");
+ } else if (c == '\t') {
+ out.append("\\t");
+ } else {
+ out.append(c);
+ }
}
out.append('"');
}
private static void appendBoolean(StringBuilder out, Boolean value) {
- if (value == null) out.append("null");
- else out.append(value.booleanValue());
+ if (value == null) {
+ out.append("null");
+ } else {
+ out.append(value.booleanValue());
+ }
}
}
From af154b262d4b0a23158c6bf32bbafdca5b497931 Mon Sep 17 00:00:00 2001
From: Shai Almog <67850168+shai-almog@users.noreply.github.com>
Date: Sun, 12 Jul 2026 15:45:54 +0300
Subject: [PATCH 07/15] Use standard copyright headings
---
Ports/WindowsPort/nativeSources/cn1_windows_accessibility.cpp | 4 ++++
.../snippets/AccessibilitySemanticsSnippets.java | 3 +++
.../ui/accessibility/AccessibilitySemanticsTest.java | 3 +++
3 files changed, 10 insertions(+)
diff --git a/Ports/WindowsPort/nativeSources/cn1_windows_accessibility.cpp b/Ports/WindowsPort/nativeSources/cn1_windows_accessibility.cpp
index bc0d40fabc2..027faafd32d 100644
--- a/Ports/WindowsPort/nativeSources/cn1_windows_accessibility.cpp
+++ b/Ports/WindowsPort/nativeSources/cn1_windows_accessibility.cpp
@@ -1,3 +1,7 @@
+/*
+ * Copyright (c) 2026 Codename One and contributors. All rights reserved.
+ */
+
/* Native virtual accessibility tree for the lightweight Win32 port. */
#ifdef _WIN32
diff --git a/docs/demos/common/src/main/java/com/codenameone/developerguide/snippets/AccessibilitySemanticsSnippets.java b/docs/demos/common/src/main/java/com/codenameone/developerguide/snippets/AccessibilitySemanticsSnippets.java
index e678f470ef7..73b914d6e6a 100644
--- a/docs/demos/common/src/main/java/com/codenameone/developerguide/snippets/AccessibilitySemanticsSnippets.java
+++ b/docs/demos/common/src/main/java/com/codenameone/developerguide/snippets/AccessibilitySemanticsSnippets.java
@@ -1,3 +1,6 @@
+/*
+ * Copyright (c) 2026 Codename One and contributors. All rights reserved.
+ */
package com.codenameone.developerguide.snippets;
import com.codename1.ui.Button;
diff --git a/maven/core-unittests/src/test/java/com/codename1/ui/accessibility/AccessibilitySemanticsTest.java b/maven/core-unittests/src/test/java/com/codename1/ui/accessibility/AccessibilitySemanticsTest.java
index f86fba2f206..9ddd5a7f7a1 100644
--- a/maven/core-unittests/src/test/java/com/codename1/ui/accessibility/AccessibilitySemanticsTest.java
+++ b/maven/core-unittests/src/test/java/com/codename1/ui/accessibility/AccessibilitySemanticsTest.java
@@ -1,3 +1,6 @@
+/*
+ * Copyright (c) 2026 Codename One and contributors. All rights reserved.
+ */
package com.codename1.ui.accessibility;
import com.codename1.junit.UITestBase;
From a03b7b3d9828df9be26c6a170329e825d1d0a88b Mon Sep 17 00:00:00 2001
From: Shai Almog <67850168+shai-almog@users.noreply.github.com>
Date: Sun, 12 Jul 2026 17:40:30 +0300
Subject: [PATCH 08/15] Use repository standard source headers
---
.../ui/accessibility/AccessibilityAction.java | 21 ++++++++++++++++++-
.../AccessibilityAssertions.java | 21 ++++++++++++++++++-
.../AccessibilityCheckedState.java | 21 ++++++++++++++++++-
.../AccessibilityChildProvider.java | 21 ++++++++++++++++++-
.../AccessibilityCollectionInfo.java | 21 ++++++++++++++++++-
.../AccessibilityCollectionItemInfo.java | 21 ++++++++++++++++++-
.../accessibility/AccessibilityGrouping.java | 21 ++++++++++++++++++-
.../accessibility/AccessibilityInspector.java | 21 ++++++++++++++++++-
.../ui/accessibility/AccessibilityIssue.java | 21 ++++++++++++++++++-
.../AccessibilityLiveRegion.java | 21 ++++++++++++++++++-
.../accessibility/AccessibilityManager.java | 21 ++++++++++++++++++-
.../ui/accessibility/AccessibilityNode.java | 21 ++++++++++++++++++-
.../AccessibilityNodeSnapshot.java | 21 ++++++++++++++++++-
.../ui/accessibility/AccessibilityRange.java | 21 ++++++++++++++++++-
.../ui/accessibility/AccessibilityRole.java | 21 ++++++++++++++++++-
.../AccessibilityTreeSnapshot.java | 21 ++++++++++++++++++-
.../ui/accessibility/package-info.java | 21 ++++++++++++++++++-
.../android/AndroidAccessibilityProvider.java | 21 ++++++++++++++++++-
.../impl/javase/JavaSEAccessibility.java | 21 ++++++++++++++++++-
.../cn1_windows_accessibility.cpp | 21 ++++++++++++++++++-
.../AccessibilitySemanticsSnippets.java | 21 ++++++++++++++++++-
.../AccessibilitySemanticsTest.java | 21 ++++++++++++++++++-
22 files changed, 440 insertions(+), 22 deletions(-)
diff --git a/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityAction.java b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityAction.java
index 9e6f67a542c..3333d3744f0 100644
--- a/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityAction.java
+++ b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityAction.java
@@ -1,5 +1,24 @@
/*
- * Copyright (c) 2026 Codename One and contributors. All rights reserved.
+ * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
*/
package com.codename1.ui.accessibility;
diff --git a/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityAssertions.java b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityAssertions.java
index de532109434..4efdccfd37a 100644
--- a/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityAssertions.java
+++ b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityAssertions.java
@@ -1,5 +1,24 @@
/*
- * Copyright (c) 2026 Codename One and contributors. All rights reserved.
+ * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
*/
package com.codename1.ui.accessibility;
diff --git a/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityCheckedState.java b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityCheckedState.java
index 4b99c355b7a..3d5fe63d714 100644
--- a/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityCheckedState.java
+++ b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityCheckedState.java
@@ -1,5 +1,24 @@
/*
- * Copyright (c) 2026 Codename One and contributors. All rights reserved.
+ * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
*/
package com.codename1.ui.accessibility;
diff --git a/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityChildProvider.java b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityChildProvider.java
index a4cea03e026..752c68ae20e 100644
--- a/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityChildProvider.java
+++ b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityChildProvider.java
@@ -1,5 +1,24 @@
/*
- * Copyright (c) 2026 Codename One and contributors. All rights reserved.
+ * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
*/
package com.codename1.ui.accessibility;
diff --git a/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityCollectionInfo.java b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityCollectionInfo.java
index e5d4b221a38..fad9b59733c 100644
--- a/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityCollectionInfo.java
+++ b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityCollectionInfo.java
@@ -1,5 +1,24 @@
/*
- * Copyright (c) 2026 Codename One and contributors. All rights reserved.
+ * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
*/
package com.codename1.ui.accessibility;
diff --git a/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityCollectionItemInfo.java b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityCollectionItemInfo.java
index a511633e7cb..75910fb0579 100644
--- a/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityCollectionItemInfo.java
+++ b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityCollectionItemInfo.java
@@ -1,5 +1,24 @@
/*
- * Copyright (c) 2026 Codename One and contributors. All rights reserved.
+ * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
*/
package com.codename1.ui.accessibility;
diff --git a/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityGrouping.java b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityGrouping.java
index 96c767ea489..26f200743e8 100644
--- a/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityGrouping.java
+++ b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityGrouping.java
@@ -1,5 +1,24 @@
/*
- * Copyright (c) 2026 Codename One and contributors. All rights reserved.
+ * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
*/
package com.codename1.ui.accessibility;
diff --git a/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityInspector.java b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityInspector.java
index cea6ae8c0fb..4c7b5c50e3f 100644
--- a/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityInspector.java
+++ b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityInspector.java
@@ -1,5 +1,24 @@
/*
- * Copyright (c) 2026 Codename One and contributors. All rights reserved.
+ * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
*/
package com.codename1.ui.accessibility;
diff --git a/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityIssue.java b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityIssue.java
index d80a0e764c9..cb05b8d6322 100644
--- a/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityIssue.java
+++ b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityIssue.java
@@ -1,5 +1,24 @@
/*
- * Copyright (c) 2026 Codename One and contributors. All rights reserved.
+ * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
*/
package com.codename1.ui.accessibility;
diff --git a/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityLiveRegion.java b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityLiveRegion.java
index 46ac6b73350..2ce8d93e9f1 100644
--- a/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityLiveRegion.java
+++ b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityLiveRegion.java
@@ -1,5 +1,24 @@
/*
- * Copyright (c) 2026 Codename One and contributors. All rights reserved.
+ * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
*/
package com.codename1.ui.accessibility;
diff --git a/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityManager.java b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityManager.java
index 88d78e3a236..aadb396c765 100644
--- a/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityManager.java
+++ b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityManager.java
@@ -1,5 +1,24 @@
/*
- * Copyright (c) 2026 Codename One and contributors. All rights reserved.
+ * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
*/
package com.codename1.ui.accessibility;
diff --git a/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityNode.java b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityNode.java
index d3ba2e2e132..8346e09d678 100644
--- a/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityNode.java
+++ b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityNode.java
@@ -1,5 +1,24 @@
/*
- * Copyright (c) 2026 Codename One and contributors. All rights reserved.
+ * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
*/
package com.codename1.ui.accessibility;
diff --git a/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityNodeSnapshot.java b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityNodeSnapshot.java
index 0b05e63f4b9..5cdbc9bb450 100644
--- a/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityNodeSnapshot.java
+++ b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityNodeSnapshot.java
@@ -1,5 +1,24 @@
/*
- * Copyright (c) 2026 Codename One and contributors. All rights reserved.
+ * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
*/
package com.codename1.ui.accessibility;
diff --git a/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityRange.java b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityRange.java
index 5a6abbe34fb..2369bb71443 100644
--- a/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityRange.java
+++ b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityRange.java
@@ -1,5 +1,24 @@
/*
- * Copyright (c) 2026 Codename One and contributors. All rights reserved.
+ * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
*/
package com.codename1.ui.accessibility;
diff --git a/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityRole.java b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityRole.java
index 7dffde79736..8349a6388e2 100644
--- a/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityRole.java
+++ b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityRole.java
@@ -1,5 +1,24 @@
/*
- * Copyright (c) 2026 Codename One and contributors. All rights reserved.
+ * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
*/
package com.codename1.ui.accessibility;
diff --git a/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityTreeSnapshot.java b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityTreeSnapshot.java
index 14595131d7c..b751aa160e6 100644
--- a/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityTreeSnapshot.java
+++ b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityTreeSnapshot.java
@@ -1,5 +1,24 @@
/*
- * Copyright (c) 2026 Codename One and contributors. All rights reserved.
+ * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
*/
package com.codename1.ui.accessibility;
diff --git a/CodenameOne/src/com/codename1/ui/accessibility/package-info.java b/CodenameOne/src/com/codename1/ui/accessibility/package-info.java
index 2168b0b820b..7857a86a70c 100644
--- a/CodenameOne/src/com/codename1/ui/accessibility/package-info.java
+++ b/CodenameOne/src/com/codename1/ui/accessibility/package-info.java
@@ -1,5 +1,24 @@
/*
- * Copyright (c) 2026 Codename One and contributors. All rights reserved.
+ * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
*/
/// Defines the portable accessibility semantics tree for Codename One's
diff --git a/Ports/Android/src/com/codename1/impl/android/AndroidAccessibilityProvider.java b/Ports/Android/src/com/codename1/impl/android/AndroidAccessibilityProvider.java
index 85e86a7d7ba..1c291bfc09d 100644
--- a/Ports/Android/src/com/codename1/impl/android/AndroidAccessibilityProvider.java
+++ b/Ports/Android/src/com/codename1/impl/android/AndroidAccessibilityProvider.java
@@ -1,5 +1,24 @@
/*
- * Copyright (c) 2026 Codename One and contributors. All rights reserved.
+ * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
*/
package com.codename1.impl.android;
diff --git a/Ports/JavaSE/src/com/codename1/impl/javase/JavaSEAccessibility.java b/Ports/JavaSE/src/com/codename1/impl/javase/JavaSEAccessibility.java
index 14b60cf0e62..312f410681e 100644
--- a/Ports/JavaSE/src/com/codename1/impl/javase/JavaSEAccessibility.java
+++ b/Ports/JavaSE/src/com/codename1/impl/javase/JavaSEAccessibility.java
@@ -1,5 +1,24 @@
/*
- * Copyright (c) 2026 Codename One and contributors. All rights reserved.
+ * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
*/
package com.codename1.impl.javase;
diff --git a/Ports/WindowsPort/nativeSources/cn1_windows_accessibility.cpp b/Ports/WindowsPort/nativeSources/cn1_windows_accessibility.cpp
index 027faafd32d..4dd96c2f93a 100644
--- a/Ports/WindowsPort/nativeSources/cn1_windows_accessibility.cpp
+++ b/Ports/WindowsPort/nativeSources/cn1_windows_accessibility.cpp
@@ -1,5 +1,24 @@
/*
- * Copyright (c) 2026 Codename One and contributors. All rights reserved.
+ * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
*/
/* Native virtual accessibility tree for the lightweight Win32 port. */
diff --git a/docs/demos/common/src/main/java/com/codenameone/developerguide/snippets/AccessibilitySemanticsSnippets.java b/docs/demos/common/src/main/java/com/codenameone/developerguide/snippets/AccessibilitySemanticsSnippets.java
index 73b914d6e6a..f86cd8e3f88 100644
--- a/docs/demos/common/src/main/java/com/codenameone/developerguide/snippets/AccessibilitySemanticsSnippets.java
+++ b/docs/demos/common/src/main/java/com/codenameone/developerguide/snippets/AccessibilitySemanticsSnippets.java
@@ -1,5 +1,24 @@
/*
- * Copyright (c) 2026 Codename One and contributors. All rights reserved.
+ * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
*/
package com.codenameone.developerguide.snippets;
diff --git a/maven/core-unittests/src/test/java/com/codename1/ui/accessibility/AccessibilitySemanticsTest.java b/maven/core-unittests/src/test/java/com/codename1/ui/accessibility/AccessibilitySemanticsTest.java
index 9ddd5a7f7a1..3b3367d8c64 100644
--- a/maven/core-unittests/src/test/java/com/codename1/ui/accessibility/AccessibilitySemanticsTest.java
+++ b/maven/core-unittests/src/test/java/com/codename1/ui/accessibility/AccessibilitySemanticsTest.java
@@ -1,5 +1,24 @@
/*
- * Copyright (c) 2026 Codename One and contributors. All rights reserved.
+ * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
*/
package com.codename1.ui.accessibility;
From e22937f852bbb9b7f410ba50bb5d31ec485a0b21 Mon Sep 17 00:00:00 2001
From: Shai Almog <67850168+shai-almog@users.noreply.github.com>
Date: Sun, 12 Jul 2026 21:17:45 +0300
Subject: [PATCH 09/15] Add accessibility display preferences and testing guide
---
.../impl/CodenameOneImplementation.java | 50 ++++
.../AccessibilityColorVisionDeficiency.java | 25 ++
CodenameOne/src/com/codename1/ui/CN.java | 50 ++++
CodenameOne/src/com/codename1/ui/Display.java | 51 ++++
.../ui/accessibility/AccessibilityAction.java | 21 +-
.../AccessibilityAssertions.java | 21 +-
.../AccessibilityCheckedState.java | 21 +-
.../AccessibilityChildProvider.java | 21 +-
.../AccessibilityCollectionInfo.java | 21 +-
.../AccessibilityCollectionItemInfo.java | 21 +-
.../accessibility/AccessibilityGrouping.java | 21 +-
.../accessibility/AccessibilityInspector.java | 21 +-
.../ui/accessibility/AccessibilityIssue.java | 21 +-
.../AccessibilityLiveRegion.java | 21 +-
.../accessibility/AccessibilityManager.java | 21 +-
.../ui/accessibility/AccessibilityNode.java | 21 +-
.../AccessibilityNodeSnapshot.java | 21 +-
.../ui/accessibility/AccessibilityRange.java | 21 +-
.../ui/accessibility/AccessibilityRole.java | 21 +-
.../AccessibilityTreeSnapshot.java | 42 ++--
.../ui/accessibility/package-info.java | 21 +-
.../android/AndroidAccessibilityProvider.java | 21 +-
.../impl/android/AndroidImplementation.java | 99 +++++++-
.../impl/javase/ComponentTreeInspector.java | 47 ++--
.../com/codename1/impl/javase/Executor.java | 4 +-
.../impl/javase/JavaSEAccessibility.java | 21 +-
.../com/codename1/impl/javase/JavaSEPort.java | 236 +++++++++++++++++-
.../impl/html5/HTML5Implementation.java | 30 +++
.../impl/linux/LinuxImplementation.java | 17 ++
.../cn1_windows_accessibility.cpp | 21 +-
.../nativeSources/cn1_windows_window.cpp | 23 ++
.../impl/windows/WindowsImplementation.java | 15 ++
.../codename1/impl/windows/WindowsNative.java | 6 +
Ports/iOSPort/nativeSources/IOSNative.m | 42 ++++
.../codename1/impl/ios/IOSImplementation.java | 45 ++++
.../src/com/codename1/impl/ios/IOSNative.java | 9 +
.../AccessibilitySemanticsSnippets.java | 79 ++++--
.../Accessibility-Semantics.asciidoc | 89 +++++++
...ccessibility-component-inspector-audit.png | Bin 0 -> 237174 bytes
.../accessibility-simulator-preferences.png | Bin 0 -> 430650 bytes
.../AccessibilitySemanticsTest.java | 41 +--
.../accessibility/AccessibilityTest.java | 27 ++
42 files changed, 957 insertions(+), 469 deletions(-)
create mode 100644 CodenameOne/src/com/codename1/ui/AccessibilityColorVisionDeficiency.java
create mode 100644 docs/developer-guide/img/accessibility-component-inspector-audit.png
create mode 100644 docs/developer-guide/img/accessibility-simulator-preferences.png
diff --git a/CodenameOne/src/com/codename1/impl/CodenameOneImplementation.java b/CodenameOne/src/com/codename1/impl/CodenameOneImplementation.java
index 70b3886b129..fa5fd97d68d 100644
--- a/CodenameOne/src/com/codename1/impl/CodenameOneImplementation.java
+++ b/CodenameOne/src/com/codename1/impl/CodenameOneImplementation.java
@@ -11177,6 +11177,56 @@ public float getLargerTextScale() {
return 1.0f;
}
+ /// Returns true when the user requests stronger foreground/background contrast.
+ public boolean isHighContrastEnabled() {
+ return false;
+ }
+
+ /// Returns true when the user requests that information isn't conveyed by color alone.
+ public boolean isDifferentiateWithoutColorEnabled() {
+ return false;
+ }
+
+ /// Returns the selected color-vision correction mode.
+ public com.codename1.ui.AccessibilityColorVisionDeficiency getColorVisionDeficiency() {
+ return com.codename1.ui.AccessibilityColorVisionDeficiency.UNKNOWN;
+ }
+
+ /// Returns true when the user requests reduced or disabled nonessential motion.
+ public boolean isReduceMotionEnabled() {
+ return false;
+ }
+
+ /// Returns true when the user requests reduced transparency and blur effects.
+ public boolean isReduceTransparencyEnabled() {
+ return false;
+ }
+
+ /// Returns true when the user requests heavier text weight.
+ public boolean isBoldTextEnabled() {
+ return false;
+ }
+
+ /// Returns true when the operating system is inverting displayed colors.
+ public boolean isInvertColorsEnabled() {
+ return false;
+ }
+
+ /// Returns true when the operating system requests a grayscale presentation.
+ public boolean isGrayscaleEnabled() {
+ return false;
+ }
+
+ /// Returns true when switches should include visible on/off labels.
+ public boolean isOnOffSwitchLabelsEnabled() {
+ return false;
+ }
+
+ /// Returns true when a screen reader or touch-exploration service is active.
+ public boolean isScreenReaderEnabled() {
+ return false;
+ }
+
/// Returns the stack trace from the exception on the given
/// thread. This API isn't supported on all platforms and may
/// return a blank string when unavailable.
diff --git a/CodenameOne/src/com/codename1/ui/AccessibilityColorVisionDeficiency.java b/CodenameOne/src/com/codename1/ui/AccessibilityColorVisionDeficiency.java
new file mode 100644
index 00000000000..d58d518765c
--- /dev/null
+++ b/CodenameOne/src/com/codename1/ui/AccessibilityColorVisionDeficiency.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (c) 2026 Codename One and contributors. All rights reserved.
+ */
+package com.codename1.ui;
+
+/// Describes a color-vision deficiency selected in the operating system or
+/// simulated by the Java SE simulator.
+///
+/// Applications should never infer that color alone is safe when this value is
+/// {@link #NONE} or {@link #UNKNOWN}. Use text, shape, iconography, or patterns
+/// in addition to color for every important distinction.
+public enum AccessibilityColorVisionDeficiency {
+ /// The platform reports that no color-vision correction is enabled.
+ NONE,
+ /// Reduced sensitivity to red light.
+ PROTANOPIA,
+ /// Reduced sensitivity to green light.
+ DEUTERANOPIA,
+ /// Reduced sensitivity to blue light.
+ TRITANOPIA,
+ /// Colors are presented as shades of gray.
+ MONOCHROMACY,
+ /// The platform doesn't expose the selected correction mode.
+ UNKNOWN
+}
diff --git a/CodenameOne/src/com/codename1/ui/CN.java b/CodenameOne/src/com/codename1/ui/CN.java
index ecabef3e823..f02c75e403d 100644
--- a/CodenameOne/src/com/codename1/ui/CN.java
+++ b/CodenameOne/src/com/codename1/ui/CN.java
@@ -1072,6 +1072,56 @@ public static Boolean isDarkMode() {
return Display.INSTANCE.isDarkMode();
}
+ /// Returns true when the user requests stronger foreground/background contrast.
+ public static boolean isHighContrastEnabled() {
+ return Display.INSTANCE.isHighContrastEnabled();
+ }
+
+ /// Returns true when the user requests that information isn't conveyed by color alone.
+ public static boolean isDifferentiateWithoutColorEnabled() {
+ return Display.INSTANCE.isDifferentiateWithoutColorEnabled();
+ }
+
+ /// Returns the selected color-vision correction mode.
+ public static AccessibilityColorVisionDeficiency getColorVisionDeficiency() {
+ return Display.INSTANCE.getColorVisionDeficiency();
+ }
+
+ /// Returns true when the user requests reduced or disabled nonessential motion.
+ public static boolean isReduceMotionEnabled() {
+ return Display.INSTANCE.isReduceMotionEnabled();
+ }
+
+ /// Returns true when the user requests reduced transparency and blur effects.
+ public static boolean isReduceTransparencyEnabled() {
+ return Display.INSTANCE.isReduceTransparencyEnabled();
+ }
+
+ /// Returns true when the user requests heavier text weight.
+ public static boolean isBoldTextEnabled() {
+ return Display.INSTANCE.isBoldTextEnabled();
+ }
+
+ /// Returns true when the operating system is inverting displayed colors.
+ public static boolean isInvertColorsEnabled() {
+ return Display.INSTANCE.isInvertColorsEnabled();
+ }
+
+ /// Returns true when the operating system requests a grayscale presentation.
+ public static boolean isGrayscaleEnabled() {
+ return Display.INSTANCE.isGrayscaleEnabled();
+ }
+
+ /// Returns true when switches should include visible on/off labels.
+ public static boolean isOnOffSwitchLabelsEnabled() {
+ return Display.INSTANCE.isOnOffSwitchLabelsEnabled();
+ }
+
+ /// Returns true when a screen reader or touch-exploration service is active.
+ public static boolean isScreenReaderEnabled() {
+ return Display.INSTANCE.isScreenReaderEnabled();
+ }
+
/// Override the default dark mode setting
///
/// #### Parameters
diff --git a/CodenameOne/src/com/codename1/ui/Display.java b/CodenameOne/src/com/codename1/ui/Display.java
index bbcde6c12f6..e22bee8d555 100644
--- a/CodenameOne/src/com/codename1/ui/Display.java
+++ b/CodenameOne/src/com/codename1/ui/Display.java
@@ -771,6 +771,57 @@ public float getLargerTextScale() {
return impl.getLargerTextScale();
}
+ /// Returns true when the user requests stronger foreground/background contrast.
+ public boolean isHighContrastEnabled() {
+ return impl.isHighContrastEnabled();
+ }
+
+ /// Returns true when the user requests that information isn't conveyed by color alone.
+ public boolean isDifferentiateWithoutColorEnabled() {
+ return impl.isDifferentiateWithoutColorEnabled();
+ }
+
+ /// Returns the selected color-vision correction, or {@link AccessibilityColorVisionDeficiency#UNKNOWN}
+ /// when the platform doesn't expose it.
+ public AccessibilityColorVisionDeficiency getColorVisionDeficiency() {
+ return impl.getColorVisionDeficiency();
+ }
+
+ /// Returns true when the user requests reduced or disabled nonessential motion.
+ public boolean isReduceMotionEnabled() {
+ return impl.isReduceMotionEnabled();
+ }
+
+ /// Returns true when the user requests reduced transparency and blur effects.
+ public boolean isReduceTransparencyEnabled() {
+ return impl.isReduceTransparencyEnabled();
+ }
+
+ /// Returns true when the user requests heavier text weight.
+ public boolean isBoldTextEnabled() {
+ return impl.isBoldTextEnabled();
+ }
+
+ /// Returns true when the operating system is inverting displayed colors.
+ public boolean isInvertColorsEnabled() {
+ return impl.isInvertColorsEnabled();
+ }
+
+ /// Returns true when the operating system requests a grayscale presentation.
+ public boolean isGrayscaleEnabled() {
+ return impl.isGrayscaleEnabled();
+ }
+
+ /// Returns true when switches should include visible on/off labels.
+ public boolean isOnOffSwitchLabelsEnabled() {
+ return impl.isOnOffSwitchLabelsEnabled();
+ }
+
+ /// Returns true when a screen reader or touch-exploration service is active.
+ public boolean isScreenReaderEnabled() {
+ return impl.isScreenReaderEnabled();
+ }
+
/// Checks if async stack traces are enabled. If enabled, the stack trace
/// at the point of `#callSerially(java.lang.Runnable)` calls will
/// be recorded, and logged in the case that there is an uncaught exception.
diff --git a/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityAction.java b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityAction.java
index 3333d3744f0..9e6f67a542c 100644
--- a/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityAction.java
+++ b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityAction.java
@@ -1,24 +1,5 @@
/*
- * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
- * This code is free software; you can redistribute it and/or modify it
- * under the terms of the GNU General Public License version 2 only, as
- * published by the Free Software Foundation. Codename One designates this
- * particular file as subject to the "Classpath" exception as provided
- * by Oracle in the LICENSE file that accompanied this code.
- *
- * This code is distributed in the hope that it will be useful, but WITHOUT
- * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
- * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
- * version 2 for more details (a copy is included in the LICENSE file that
- * accompanied this code).
- *
- * You should have received a copy of the GNU General Public License version
- * 2 along with this work; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
- *
- * Please contact Codename One through http://www.codenameone.com/ if you
- * need additional information or have any questions.
+ * Copyright (c) 2026 Codename One and contributors. All rights reserved.
*/
package com.codename1.ui.accessibility;
diff --git a/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityAssertions.java b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityAssertions.java
index 4efdccfd37a..de532109434 100644
--- a/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityAssertions.java
+++ b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityAssertions.java
@@ -1,24 +1,5 @@
/*
- * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
- * This code is free software; you can redistribute it and/or modify it
- * under the terms of the GNU General Public License version 2 only, as
- * published by the Free Software Foundation. Codename One designates this
- * particular file as subject to the "Classpath" exception as provided
- * by Oracle in the LICENSE file that accompanied this code.
- *
- * This code is distributed in the hope that it will be useful, but WITHOUT
- * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
- * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
- * version 2 for more details (a copy is included in the LICENSE file that
- * accompanied this code).
- *
- * You should have received a copy of the GNU General Public License version
- * 2 along with this work; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
- *
- * Please contact Codename One through http://www.codenameone.com/ if you
- * need additional information or have any questions.
+ * Copyright (c) 2026 Codename One and contributors. All rights reserved.
*/
package com.codename1.ui.accessibility;
diff --git a/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityCheckedState.java b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityCheckedState.java
index 3d5fe63d714..4b99c355b7a 100644
--- a/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityCheckedState.java
+++ b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityCheckedState.java
@@ -1,24 +1,5 @@
/*
- * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
- * This code is free software; you can redistribute it and/or modify it
- * under the terms of the GNU General Public License version 2 only, as
- * published by the Free Software Foundation. Codename One designates this
- * particular file as subject to the "Classpath" exception as provided
- * by Oracle in the LICENSE file that accompanied this code.
- *
- * This code is distributed in the hope that it will be useful, but WITHOUT
- * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
- * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
- * version 2 for more details (a copy is included in the LICENSE file that
- * accompanied this code).
- *
- * You should have received a copy of the GNU General Public License version
- * 2 along with this work; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
- *
- * Please contact Codename One through http://www.codenameone.com/ if you
- * need additional information or have any questions.
+ * Copyright (c) 2026 Codename One and contributors. All rights reserved.
*/
package com.codename1.ui.accessibility;
diff --git a/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityChildProvider.java b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityChildProvider.java
index 752c68ae20e..a4cea03e026 100644
--- a/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityChildProvider.java
+++ b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityChildProvider.java
@@ -1,24 +1,5 @@
/*
- * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
- * This code is free software; you can redistribute it and/or modify it
- * under the terms of the GNU General Public License version 2 only, as
- * published by the Free Software Foundation. Codename One designates this
- * particular file as subject to the "Classpath" exception as provided
- * by Oracle in the LICENSE file that accompanied this code.
- *
- * This code is distributed in the hope that it will be useful, but WITHOUT
- * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
- * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
- * version 2 for more details (a copy is included in the LICENSE file that
- * accompanied this code).
- *
- * You should have received a copy of the GNU General Public License version
- * 2 along with this work; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
- *
- * Please contact Codename One through http://www.codenameone.com/ if you
- * need additional information or have any questions.
+ * Copyright (c) 2026 Codename One and contributors. All rights reserved.
*/
package com.codename1.ui.accessibility;
diff --git a/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityCollectionInfo.java b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityCollectionInfo.java
index fad9b59733c..e5d4b221a38 100644
--- a/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityCollectionInfo.java
+++ b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityCollectionInfo.java
@@ -1,24 +1,5 @@
/*
- * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
- * This code is free software; you can redistribute it and/or modify it
- * under the terms of the GNU General Public License version 2 only, as
- * published by the Free Software Foundation. Codename One designates this
- * particular file as subject to the "Classpath" exception as provided
- * by Oracle in the LICENSE file that accompanied this code.
- *
- * This code is distributed in the hope that it will be useful, but WITHOUT
- * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
- * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
- * version 2 for more details (a copy is included in the LICENSE file that
- * accompanied this code).
- *
- * You should have received a copy of the GNU General Public License version
- * 2 along with this work; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
- *
- * Please contact Codename One through http://www.codenameone.com/ if you
- * need additional information or have any questions.
+ * Copyright (c) 2026 Codename One and contributors. All rights reserved.
*/
package com.codename1.ui.accessibility;
diff --git a/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityCollectionItemInfo.java b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityCollectionItemInfo.java
index 75910fb0579..a511633e7cb 100644
--- a/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityCollectionItemInfo.java
+++ b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityCollectionItemInfo.java
@@ -1,24 +1,5 @@
/*
- * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
- * This code is free software; you can redistribute it and/or modify it
- * under the terms of the GNU General Public License version 2 only, as
- * published by the Free Software Foundation. Codename One designates this
- * particular file as subject to the "Classpath" exception as provided
- * by Oracle in the LICENSE file that accompanied this code.
- *
- * This code is distributed in the hope that it will be useful, but WITHOUT
- * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
- * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
- * version 2 for more details (a copy is included in the LICENSE file that
- * accompanied this code).
- *
- * You should have received a copy of the GNU General Public License version
- * 2 along with this work; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
- *
- * Please contact Codename One through http://www.codenameone.com/ if you
- * need additional information or have any questions.
+ * Copyright (c) 2026 Codename One and contributors. All rights reserved.
*/
package com.codename1.ui.accessibility;
diff --git a/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityGrouping.java b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityGrouping.java
index 26f200743e8..96c767ea489 100644
--- a/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityGrouping.java
+++ b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityGrouping.java
@@ -1,24 +1,5 @@
/*
- * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
- * This code is free software; you can redistribute it and/or modify it
- * under the terms of the GNU General Public License version 2 only, as
- * published by the Free Software Foundation. Codename One designates this
- * particular file as subject to the "Classpath" exception as provided
- * by Oracle in the LICENSE file that accompanied this code.
- *
- * This code is distributed in the hope that it will be useful, but WITHOUT
- * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
- * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
- * version 2 for more details (a copy is included in the LICENSE file that
- * accompanied this code).
- *
- * You should have received a copy of the GNU General Public License version
- * 2 along with this work; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
- *
- * Please contact Codename One through http://www.codenameone.com/ if you
- * need additional information or have any questions.
+ * Copyright (c) 2026 Codename One and contributors. All rights reserved.
*/
package com.codename1.ui.accessibility;
diff --git a/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityInspector.java b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityInspector.java
index 4c7b5c50e3f..cea6ae8c0fb 100644
--- a/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityInspector.java
+++ b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityInspector.java
@@ -1,24 +1,5 @@
/*
- * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
- * This code is free software; you can redistribute it and/or modify it
- * under the terms of the GNU General Public License version 2 only, as
- * published by the Free Software Foundation. Codename One designates this
- * particular file as subject to the "Classpath" exception as provided
- * by Oracle in the LICENSE file that accompanied this code.
- *
- * This code is distributed in the hope that it will be useful, but WITHOUT
- * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
- * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
- * version 2 for more details (a copy is included in the LICENSE file that
- * accompanied this code).
- *
- * You should have received a copy of the GNU General Public License version
- * 2 along with this work; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
- *
- * Please contact Codename One through http://www.codenameone.com/ if you
- * need additional information or have any questions.
+ * Copyright (c) 2026 Codename One and contributors. All rights reserved.
*/
package com.codename1.ui.accessibility;
diff --git a/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityIssue.java b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityIssue.java
index cb05b8d6322..d80a0e764c9 100644
--- a/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityIssue.java
+++ b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityIssue.java
@@ -1,24 +1,5 @@
/*
- * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
- * This code is free software; you can redistribute it and/or modify it
- * under the terms of the GNU General Public License version 2 only, as
- * published by the Free Software Foundation. Codename One designates this
- * particular file as subject to the "Classpath" exception as provided
- * by Oracle in the LICENSE file that accompanied this code.
- *
- * This code is distributed in the hope that it will be useful, but WITHOUT
- * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
- * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
- * version 2 for more details (a copy is included in the LICENSE file that
- * accompanied this code).
- *
- * You should have received a copy of the GNU General Public License version
- * 2 along with this work; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
- *
- * Please contact Codename One through http://www.codenameone.com/ if you
- * need additional information or have any questions.
+ * Copyright (c) 2026 Codename One and contributors. All rights reserved.
*/
package com.codename1.ui.accessibility;
diff --git a/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityLiveRegion.java b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityLiveRegion.java
index 2ce8d93e9f1..46ac6b73350 100644
--- a/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityLiveRegion.java
+++ b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityLiveRegion.java
@@ -1,24 +1,5 @@
/*
- * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
- * This code is free software; you can redistribute it and/or modify it
- * under the terms of the GNU General Public License version 2 only, as
- * published by the Free Software Foundation. Codename One designates this
- * particular file as subject to the "Classpath" exception as provided
- * by Oracle in the LICENSE file that accompanied this code.
- *
- * This code is distributed in the hope that it will be useful, but WITHOUT
- * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
- * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
- * version 2 for more details (a copy is included in the LICENSE file that
- * accompanied this code).
- *
- * You should have received a copy of the GNU General Public License version
- * 2 along with this work; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
- *
- * Please contact Codename One through http://www.codenameone.com/ if you
- * need additional information or have any questions.
+ * Copyright (c) 2026 Codename One and contributors. All rights reserved.
*/
package com.codename1.ui.accessibility;
diff --git a/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityManager.java b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityManager.java
index aadb396c765..88d78e3a236 100644
--- a/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityManager.java
+++ b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityManager.java
@@ -1,24 +1,5 @@
/*
- * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
- * This code is free software; you can redistribute it and/or modify it
- * under the terms of the GNU General Public License version 2 only, as
- * published by the Free Software Foundation. Codename One designates this
- * particular file as subject to the "Classpath" exception as provided
- * by Oracle in the LICENSE file that accompanied this code.
- *
- * This code is distributed in the hope that it will be useful, but WITHOUT
- * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
- * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
- * version 2 for more details (a copy is included in the LICENSE file that
- * accompanied this code).
- *
- * You should have received a copy of the GNU General Public License version
- * 2 along with this work; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
- *
- * Please contact Codename One through http://www.codenameone.com/ if you
- * need additional information or have any questions.
+ * Copyright (c) 2026 Codename One and contributors. All rights reserved.
*/
package com.codename1.ui.accessibility;
diff --git a/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityNode.java b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityNode.java
index 8346e09d678..d3ba2e2e132 100644
--- a/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityNode.java
+++ b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityNode.java
@@ -1,24 +1,5 @@
/*
- * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
- * This code is free software; you can redistribute it and/or modify it
- * under the terms of the GNU General Public License version 2 only, as
- * published by the Free Software Foundation. Codename One designates this
- * particular file as subject to the "Classpath" exception as provided
- * by Oracle in the LICENSE file that accompanied this code.
- *
- * This code is distributed in the hope that it will be useful, but WITHOUT
- * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
- * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
- * version 2 for more details (a copy is included in the LICENSE file that
- * accompanied this code).
- *
- * You should have received a copy of the GNU General Public License version
- * 2 along with this work; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
- *
- * Please contact Codename One through http://www.codenameone.com/ if you
- * need additional information or have any questions.
+ * Copyright (c) 2026 Codename One and contributors. All rights reserved.
*/
package com.codename1.ui.accessibility;
diff --git a/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityNodeSnapshot.java b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityNodeSnapshot.java
index 5cdbc9bb450..0b05e63f4b9 100644
--- a/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityNodeSnapshot.java
+++ b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityNodeSnapshot.java
@@ -1,24 +1,5 @@
/*
- * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
- * This code is free software; you can redistribute it and/or modify it
- * under the terms of the GNU General Public License version 2 only, as
- * published by the Free Software Foundation. Codename One designates this
- * particular file as subject to the "Classpath" exception as provided
- * by Oracle in the LICENSE file that accompanied this code.
- *
- * This code is distributed in the hope that it will be useful, but WITHOUT
- * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
- * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
- * version 2 for more details (a copy is included in the LICENSE file that
- * accompanied this code).
- *
- * You should have received a copy of the GNU General Public License version
- * 2 along with this work; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
- *
- * Please contact Codename One through http://www.codenameone.com/ if you
- * need additional information or have any questions.
+ * Copyright (c) 2026 Codename One and contributors. All rights reserved.
*/
package com.codename1.ui.accessibility;
diff --git a/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityRange.java b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityRange.java
index 2369bb71443..5a6abbe34fb 100644
--- a/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityRange.java
+++ b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityRange.java
@@ -1,24 +1,5 @@
/*
- * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
- * This code is free software; you can redistribute it and/or modify it
- * under the terms of the GNU General Public License version 2 only, as
- * published by the Free Software Foundation. Codename One designates this
- * particular file as subject to the "Classpath" exception as provided
- * by Oracle in the LICENSE file that accompanied this code.
- *
- * This code is distributed in the hope that it will be useful, but WITHOUT
- * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
- * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
- * version 2 for more details (a copy is included in the LICENSE file that
- * accompanied this code).
- *
- * You should have received a copy of the GNU General Public License version
- * 2 along with this work; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
- *
- * Please contact Codename One through http://www.codenameone.com/ if you
- * need additional information or have any questions.
+ * Copyright (c) 2026 Codename One and contributors. All rights reserved.
*/
package com.codename1.ui.accessibility;
diff --git a/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityRole.java b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityRole.java
index 8349a6388e2..7dffde79736 100644
--- a/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityRole.java
+++ b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityRole.java
@@ -1,24 +1,5 @@
/*
- * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
- * This code is free software; you can redistribute it and/or modify it
- * under the terms of the GNU General Public License version 2 only, as
- * published by the Free Software Foundation. Codename One designates this
- * particular file as subject to the "Classpath" exception as provided
- * by Oracle in the LICENSE file that accompanied this code.
- *
- * This code is distributed in the hope that it will be useful, but WITHOUT
- * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
- * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
- * version 2 for more details (a copy is included in the LICENSE file that
- * accompanied this code).
- *
- * You should have received a copy of the GNU General Public License version
- * 2 along with this work; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
- *
- * Please contact Codename One through http://www.codenameone.com/ if you
- * need additional information or have any questions.
+ * Copyright (c) 2026 Codename One and contributors. All rights reserved.
*/
package com.codename1.ui.accessibility;
diff --git a/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityTreeSnapshot.java b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityTreeSnapshot.java
index b751aa160e6..1a1d7804a17 100644
--- a/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityTreeSnapshot.java
+++ b/CodenameOne/src/com/codename1/ui/accessibility/AccessibilityTreeSnapshot.java
@@ -1,24 +1,5 @@
/*
- * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
- * This code is free software; you can redistribute it and/or modify it
- * under the terms of the GNU General Public License version 2 only, as
- * published by the Free Software Foundation. Codename One designates this
- * particular file as subject to the "Classpath" exception as provided
- * by Oracle in the LICENSE file that accompanied this code.
- *
- * This code is distributed in the hope that it will be useful, but WITHOUT
- * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
- * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
- * version 2 for more details (a copy is included in the LICENSE file that
- * accompanied this code).
- *
- * You should have received a copy of the GNU General Public License version
- * 2 along with this work; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
- *
- * Please contact Codename One through http://www.codenameone.com/ if you
- * need additional information or have any questions.
+ * Copyright (c) 2026 Codename One and contributors. All rights reserved.
*/
package com.codename1.ui.accessibility;
@@ -54,6 +35,27 @@ public AccessibilityNodeSnapshot getNode(long id) {
return nodes.get(Long.valueOf(id));
}
+ /// Finds a node by its application-supplied stable identifier.
+ ///
+ /// #### Parameters
+ ///
+ /// - `identifier`: identifier assigned with `AccessibilityNode#setIdentifier(String)`
+ ///
+ /// #### Returns
+ ///
+ /// the matching node, or null when no node has that identifier.
+ public AccessibilityNodeSnapshot getNodeByIdentifier(String identifier) {
+ if (identifier == null) {
+ return null;
+ }
+ for (AccessibilityNodeSnapshot node : nodes.values()) {
+ if (identifier.equals(node.getIdentifier())) {
+ return node;
+ }
+ }
+ return null;
+ }
+
/// Returns the deepest semantic node containing the screen coordinate.
public AccessibilityNodeSnapshot getNodeAt(int x, int y) {
AccessibilityNodeSnapshot best = null;
diff --git a/CodenameOne/src/com/codename1/ui/accessibility/package-info.java b/CodenameOne/src/com/codename1/ui/accessibility/package-info.java
index 7857a86a70c..2168b0b820b 100644
--- a/CodenameOne/src/com/codename1/ui/accessibility/package-info.java
+++ b/CodenameOne/src/com/codename1/ui/accessibility/package-info.java
@@ -1,24 +1,5 @@
/*
- * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
- * This code is free software; you can redistribute it and/or modify it
- * under the terms of the GNU General Public License version 2 only, as
- * published by the Free Software Foundation. Codename One designates this
- * particular file as subject to the "Classpath" exception as provided
- * by Oracle in the LICENSE file that accompanied this code.
- *
- * This code is distributed in the hope that it will be useful, but WITHOUT
- * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
- * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
- * version 2 for more details (a copy is included in the LICENSE file that
- * accompanied this code).
- *
- * You should have received a copy of the GNU General Public License version
- * 2 along with this work; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
- *
- * Please contact Codename One through http://www.codenameone.com/ if you
- * need additional information or have any questions.
+ * Copyright (c) 2026 Codename One and contributors. All rights reserved.
*/
/// Defines the portable accessibility semantics tree for Codename One's
diff --git a/Ports/Android/src/com/codename1/impl/android/AndroidAccessibilityProvider.java b/Ports/Android/src/com/codename1/impl/android/AndroidAccessibilityProvider.java
index 1c291bfc09d..85e86a7d7ba 100644
--- a/Ports/Android/src/com/codename1/impl/android/AndroidAccessibilityProvider.java
+++ b/Ports/Android/src/com/codename1/impl/android/AndroidAccessibilityProvider.java
@@ -1,24 +1,5 @@
/*
- * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
- * This code is free software; you can redistribute it and/or modify it
- * under the terms of the GNU General Public License version 2 only, as
- * published by the Free Software Foundation. Codename One designates this
- * particular file as subject to the "Classpath" exception as provided
- * by Oracle in the LICENSE file that accompanied this code.
- *
- * This code is distributed in the hope that it will be useful, but WITHOUT
- * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
- * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
- * version 2 for more details (a copy is included in the LICENSE file that
- * accompanied this code).
- *
- * You should have received a copy of the GNU General Public License version
- * 2 along with this work; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
- *
- * Please contact Codename One through http://www.codenameone.com/ if you
- * need additional information or have any questions.
+ * Copyright (c) 2026 Codename One and contributors. All rights reserved.
*/
package com.codename1.impl.android;
diff --git a/Ports/Android/src/com/codename1/impl/android/AndroidImplementation.java b/Ports/Android/src/com/codename1/impl/android/AndroidImplementation.java
index 17feb3e03ab..9b57c0ed51d 100644
--- a/Ports/Android/src/com/codename1/impl/android/AndroidImplementation.java
+++ b/Ports/Android/src/com/codename1/impl/android/AndroidImplementation.java
@@ -53,21 +53,24 @@
import android.media.AudioManager;
import android.net.Uri;
import android.os.Vibrator;
-import android.os.PowerManager;
+import android.os.PowerManager;
+import android.provider.Settings;
import android.telephony.TelephonyManager;
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.TypedValue;
import android.view.KeyEvent;
import android.view.View;
-import android.view.ViewGroup;
+import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityManager;
import android.view.Window;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.RelativeLayout;
import android.widget.TextView;
-import com.codename1.ui.BrowserComponent;
+import com.codename1.ui.BrowserComponent;
+import com.codename1.ui.AccessibilityColorVisionDeficiency;
import com.codename1.ui.Component;
import com.codename1.ui.Font;
@@ -1396,7 +1399,7 @@ public boolean isLargerTextEnabled() {
}
@Override
- public float getLargerTextScale() {
+ public float getLargerTextScale() {
try {
Configuration configuration;
if (getActivity() != null) {
@@ -12939,6 +12942,94 @@ public void run() {
});
}
+ @Override
+ public boolean isHighContrastEnabled() {
+ try {
+ AccessibilityManager manager = (AccessibilityManager)getContext()
+ .getSystemService(Context.ACCESSIBILITY_SERVICE);
+ return android.os.Build.VERSION.SDK_INT >= 21 && manager != null
+ && manager.isHighTextContrastEnabled();
+ } catch (Throwable t) {
+ return secureSettingEnabled("high_text_contrast_enabled")
+ || secureSettingEnabled("accessibility_display_high_text_contrast_enabled");
+ }
+ }
+
+ @Override
+ public boolean isDifferentiateWithoutColorEnabled() {
+ return secureSettingEnabled("accessibility_display_daltonizer_enabled");
+ }
+
+ @Override
+ public AccessibilityColorVisionDeficiency getColorVisionDeficiency() {
+ if (!secureSettingEnabled("accessibility_display_daltonizer_enabled")) {
+ return AccessibilityColorVisionDeficiency.NONE;
+ }
+ try {
+ int mode = Settings.Secure.getInt(getContext().getContentResolver(),
+ "accessibility_display_daltonizer");
+ switch (mode) {
+ case 0: return AccessibilityColorVisionDeficiency.MONOCHROMACY;
+ case 11: return AccessibilityColorVisionDeficiency.PROTANOPIA;
+ case 12: return AccessibilityColorVisionDeficiency.DEUTERANOPIA;
+ case 13: return AccessibilityColorVisionDeficiency.TRITANOPIA;
+ default: return AccessibilityColorVisionDeficiency.UNKNOWN;
+ }
+ } catch (Throwable t) {
+ return AccessibilityColorVisionDeficiency.UNKNOWN;
+ }
+ }
+
+ @Override
+ public boolean isReduceMotionEnabled() {
+ try {
+ return Settings.Global.getFloat(getContext().getContentResolver(),
+ Settings.Global.ANIMATOR_DURATION_SCALE, 1f) == 0f;
+ } catch (Throwable t) {
+ return false;
+ }
+ }
+
+ @Override
+ public boolean isBoldTextEnabled() {
+ try {
+ Object value = Configuration.class.getField("fontWeightAdjustment")
+ .get(getContext().getResources().getConfiguration());
+ return value instanceof Integer && ((Integer)value).intValue() >= 300;
+ } catch (Throwable t) {
+ return false;
+ }
+ }
+
+ @Override
+ public boolean isInvertColorsEnabled() {
+ return secureSettingEnabled("accessibility_display_inversion_enabled");
+ }
+
+ @Override
+ public boolean isGrayscaleEnabled() {
+ return getColorVisionDeficiency() == AccessibilityColorVisionDeficiency.MONOCHROMACY;
+ }
+
+ @Override
+ public boolean isScreenReaderEnabled() {
+ try {
+ AccessibilityManager manager = (AccessibilityManager)getContext()
+ .getSystemService(Context.ACCESSIBILITY_SERVICE);
+ return manager != null && manager.isEnabled() && manager.isTouchExplorationEnabled();
+ } catch (Throwable t) {
+ return false;
+ }
+ }
+
+ private boolean secureSettingEnabled(String key) {
+ try {
+ return Settings.Secure.getInt(getContext().getContentResolver(), key, 0) == 1;
+ } catch (Throwable t) {
+ return false;
+ }
+ }
+
@Override
public void accessibilityTreeChanged(final int changeType) {
final Activity act = getActivity();
diff --git a/Ports/JavaSE/src/com/codename1/impl/javase/ComponentTreeInspector.java b/Ports/JavaSE/src/com/codename1/impl/javase/ComponentTreeInspector.java
index 4cd8bbcfad2..c5738bf3af0 100644
--- a/Ports/JavaSE/src/com/codename1/impl/javase/ComponentTreeInspector.java
+++ b/Ports/JavaSE/src/com/codename1/impl/javase/ComponentTreeInspector.java
@@ -109,6 +109,31 @@ public JFrame showInFrame() {
frame.setVisible(true);
return frame;
}
+
+ /** Audits the current form and displays the result. */
+ public void showAccessibilityAudit() {
+ com.codename1.ui.CN.callSerially(new Runnable() {
+ public void run() {
+ Form form = Display.getInstance().getCurrent();
+ if (form == null) return;
+ AccessibilityTreeSnapshot snapshot = AccessibilityInspector.snapshot(form);
+ final List issues = AccessibilityAssertions.audit(snapshot);
+ final StringBuilder message = new StringBuilder();
+ for (AccessibilityIssue issue : issues) {
+ if (message.length() > 0) message.append('\n');
+ message.append(issue.toString());
+ }
+ SwingUtilities.invokeLater(new Runnable() {
+ public void run() {
+ JOptionPane.showMessageDialog(ComponentTreeInspector.this,
+ issues.isEmpty() ? "No accessibility issues found" : message.toString(),
+ "Accessibility Audit", issues.isEmpty()
+ ? JOptionPane.INFORMATION_MESSAGE : JOptionPane.WARNING_MESSAGE);
+ }
+ });
+ }
+ });
+ }
public void dispose() {
if (frame != null) {
@@ -208,27 +233,7 @@ public void run() {
JMenuItem auditAccessibilityTree = new JMenuItem("Audit Accessibility Tree");
auditAccessibilityTree.addActionListener(new java.awt.event.ActionListener() {
public void actionPerformed(java.awt.event.ActionEvent evt) {
- com.codename1.ui.CN.callSerially(new Runnable() {
- public void run() {
- Form form = Display.getInstance().getCurrent();
- if (form == null) return;
- AccessibilityTreeSnapshot snapshot = AccessibilityInspector.snapshot(form);
- final List issues = AccessibilityAssertions.audit(snapshot);
- final StringBuilder message = new StringBuilder();
- for (AccessibilityIssue issue : issues) {
- if (message.length() > 0) message.append('\n');
- message.append(issue.toString());
- }
- SwingUtilities.invokeLater(new Runnable() {
- public void run() {
- JOptionPane.showMessageDialog(ComponentTreeInspector.this,
- issues.isEmpty() ? "No accessibility issues found" : message.toString(),
- "Accessibility Audit", issues.isEmpty()
- ? JOptionPane.INFORMATION_MESSAGE : JOptionPane.WARNING_MESSAGE);
- }
- });
- }
- });
+ showAccessibilityAudit();
}
});
contextMenu.add(auditAccessibilityTree);
diff --git a/Ports/JavaSE/src/com/codename1/impl/javase/Executor.java b/Ports/JavaSE/src/com/codename1/impl/javase/Executor.java
index bc57753c841..80badf36737 100644
--- a/Ports/JavaSE/src/com/codename1/impl/javase/Executor.java
+++ b/Ports/JavaSE/src/com/codename1/impl/javase/Executor.java
@@ -76,7 +76,9 @@ public class Executor {
String n = System.getProperty("os.name");
if (n != null && n.startsWith("Mac")) {
IS_MAC = true;
- System.setProperty("apple.laf.useScreenMenuBar", "true");
+ if (System.getProperty("apple.laf.useScreenMenuBar") == null) {
+ System.setProperty("apple.laf.useScreenMenuBar", "true");
+ }
System.setProperty("com.apple.mrj.application.apple.menu.about.name", "Codename One GUIBuilder");
System.setProperty("apple.eawt.quitStrategy", "CLOSE_ALL_WINDOWS");
} else {
diff --git a/Ports/JavaSE/src/com/codename1/impl/javase/JavaSEAccessibility.java b/Ports/JavaSE/src/com/codename1/impl/javase/JavaSEAccessibility.java
index 312f410681e..14b60cf0e62 100644
--- a/Ports/JavaSE/src/com/codename1/impl/javase/JavaSEAccessibility.java
+++ b/Ports/JavaSE/src/com/codename1/impl/javase/JavaSEAccessibility.java
@@ -1,24 +1,5 @@
/*
- * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
- * This code is free software; you can redistribute it and/or modify it
- * under the terms of the GNU General Public License version 2 only, as
- * published by the Free Software Foundation. Codename One designates this
- * particular file as subject to the "Classpath" exception as provided
- * by Oracle in the LICENSE file that accompanied this code.
- *
- * This code is distributed in the hope that it will be useful, but WITHOUT
- * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
- * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
- * version 2 for more details (a copy is included in the LICENSE file that
- * accompanied this code).
- *
- * You should have received a copy of the GNU General Public License version
- * 2 along with this work; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
- *
- * Please contact Codename One through http://www.codenameone.com/ if you
- * need additional information or have any questions.
+ * Copyright (c) 2026 Codename One and contributors. All rights reserved.
*/
package com.codename1.impl.javase;
diff --git a/Ports/JavaSE/src/com/codename1/impl/javase/JavaSEPort.java b/Ports/JavaSE/src/com/codename1/impl/javase/JavaSEPort.java
index e9572232cfd..ed137c4dbfa 100644
--- a/Ports/JavaSE/src/com/codename1/impl/javase/JavaSEPort.java
+++ b/Ports/JavaSE/src/com/codename1/impl/javase/JavaSEPort.java
@@ -39,6 +39,7 @@
import com.codename1.payment.PromotionalOffer;
import com.codename1.printing.PrintResult;
import com.codename1.printing.PrintResultListener;
+import com.codename1.ui.AccessibilityColorVisionDeficiency;
import com.codename1.ui.Component;
import com.codename1.ui.Display;
import com.codename1.ui.Font;
@@ -235,6 +236,17 @@ public class JavaSEPort extends CodenameOneImplementation {
// font scaling, so the Larger Text menu only nags them a single time per session.
private boolean largerTextOptInWarningShown = false;
private static final String PREF_LARGER_TEXT_SCALE = "cn1.simulator.largerTextScale";
+ private static final String PREF_ACCESSIBILITY_PREFIX = "cn1.simulator.accessibility.";
+ private boolean highContrastEnabled;
+ private boolean differentiateWithoutColorEnabled;
+ private boolean reduceMotionEnabled;
+ private boolean reduceTransparencyEnabled;
+ private boolean boldTextEnabled;
+ private boolean invertColorsEnabled;
+ private boolean grayscaleEnabled;
+ private boolean onOffSwitchLabelsEnabled;
+ private boolean screenReaderEnabled;
+ private AccessibilityColorVisionDeficiency colorVisionDeficiency = AccessibilityColorVisionDeficiency.NONE;
// Floor below which any persisted window dimension is treated as the
// product of a layout glitch (eg. a pack() with a collapsed canvas, an
@@ -611,7 +623,9 @@ public static double getRetinaScale() {
IS_MAC = false;
}
isWindows = File.separatorChar == '\\';
- System.setProperty("apple.laf.useScreenMenuBar", "true");
+ if (System.getProperty("apple.laf.useScreenMenuBar") == null) {
+ System.setProperty("apple.laf.useScreenMenuBar", "true");
+ }
if(isWindows) {
fontFaceSystem = "ArialUnicodeMS";
@@ -2880,7 +2894,9 @@ private boolean drawScreenBuffer(java.awt.Graphics g) {
}
if (isEnabled()) {
- g.drawImage(buffer, (int) ((getScreenCoordinates().getX() + x) * zoomLevel), (int) ((getScreenCoordinates().getY() + y) * zoomLevel), this);
+ g.drawImage(accessibilityFilteredImage(buffer),
+ (int) ((getScreenCoordinates().getX() + x) * zoomLevel),
+ (int) ((getScreenCoordinates().getY() + y) * zoomLevel), this);
} else {
g.setColor(Color.WHITE);
g.fillRect(x + (int) (getSkin().getWidth() * zoomLevel), y, getWidth(), getHeight());
@@ -2933,7 +2949,7 @@ private boolean drawScreenBuffer(java.awt.Graphics g) {
bg.dispose();
painted = true;
}
- g.drawImage(buffer, x, y, this);
+ g.drawImage(accessibilityFilteredImage(buffer), x, y, this);
}
((Graphics2D)g).setTransform(t);
return painted;
@@ -6409,6 +6425,7 @@ public void actionPerformed(ActionEvent e) {
});
final JMenu largerTextMenu = installLargerTextMenu(simulateMenu, pref, frm);
+ final JMenu accessibilityPreferencesMenu = installAccessibilityPreferencesMenu(pref);
final JMenu notificationBackgroundMenu = installNotificationBackgroundSimulationMenu(simulateMenu);
@@ -6619,6 +6636,7 @@ public void actionPerformed(ActionEvent e) {
simulateMenu.addSeparator();
simulateMenu.add(darkLightModeMenu);
simulateMenu.add(largerTextMenu);
+ simulateMenu.add(accessibilityPreferencesMenu);
simulateMenu.add(purchaseMenu);
simulateMenu.add(permFlag);
simulateMenu.add(notificationBackgroundMenu);
@@ -7576,6 +7594,113 @@ public void actionPerformed(ActionEvent e) {
return largerTextMenu;
}
+ private JMenu installAccessibilityPreferencesMenu(final Preferences pref) {
+ highContrastEnabled = pref.getBoolean(PREF_ACCESSIBILITY_PREFIX + "highContrast", false);
+ differentiateWithoutColorEnabled = pref.getBoolean(
+ PREF_ACCESSIBILITY_PREFIX + "differentiateWithoutColor", false);
+ reduceMotionEnabled = pref.getBoolean(PREF_ACCESSIBILITY_PREFIX + "reduceMotion", false);
+ reduceTransparencyEnabled = pref.getBoolean(PREF_ACCESSIBILITY_PREFIX + "reduceTransparency", false);
+ boldTextEnabled = pref.getBoolean(PREF_ACCESSIBILITY_PREFIX + "boldText", false);
+ invertColorsEnabled = pref.getBoolean(PREF_ACCESSIBILITY_PREFIX + "invertColors", false);
+ grayscaleEnabled = pref.getBoolean(PREF_ACCESSIBILITY_PREFIX + "grayscale", false);
+ onOffSwitchLabelsEnabled = pref.getBoolean(PREF_ACCESSIBILITY_PREFIX + "switchLabels", false);
+ screenReaderEnabled = pref.getBoolean(PREF_ACCESSIBILITY_PREFIX + "screenReader", false);
+ try {
+ colorVisionDeficiency = AccessibilityColorVisionDeficiency.valueOf(
+ pref.get(PREF_ACCESSIBILITY_PREFIX + "colorVision",
+ AccessibilityColorVisionDeficiency.NONE.name()));
+ } catch (IllegalArgumentException ex) {
+ colorVisionDeficiency = AccessibilityColorVisionDeficiency.NONE;
+ }
+
+ JMenu menu = new JMenu("Accessibility Preferences");
+ registerMenuWithBlit(menu);
+ menu.add(accessibilityPreferenceItem("High Contrast", highContrastEnabled, pref, "highContrast", 0));
+ menu.add(accessibilityPreferenceItem("Differentiate Without Color",
+ differentiateWithoutColorEnabled, pref, "differentiateWithoutColor", 1));
+ menu.add(createColorVisionMenu(pref));
+ menu.addSeparator();
+ menu.add(accessibilityPreferenceItem("Reduce Motion", reduceMotionEnabled, pref, "reduceMotion", 2));
+ menu.add(accessibilityPreferenceItem("Reduce Transparency", reduceTransparencyEnabled,
+ pref, "reduceTransparency", 3));
+ menu.add(accessibilityPreferenceItem("Bold Text", boldTextEnabled, pref, "boldText", 4));
+ menu.add(accessibilityPreferenceItem("Invert Colors", invertColorsEnabled, pref, "invertColors", 5));
+ menu.add(accessibilityPreferenceItem("Grayscale", grayscaleEnabled, pref, "grayscale", 6));
+ menu.add(accessibilityPreferenceItem("On/Off Switch Labels", onOffSwitchLabelsEnabled,
+ pref, "switchLabels", 7));
+ menu.add(accessibilityPreferenceItem("Screen Reader Active", screenReaderEnabled,
+ pref, "screenReader", 8));
+ return menu;
+ }
+
+ private JCheckBoxMenuItem accessibilityPreferenceItem(String label, boolean selected,
+ final Preferences pref, final String key, final int preference) {
+ final JCheckBoxMenuItem item = new JCheckBoxMenuItem(label, selected);
+ item.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent event) {
+ setSimulatorAccessibilityPreference(preference, item.isSelected());
+ pref.putBoolean(PREF_ACCESSIBILITY_PREFIX + key, item.isSelected());
+ refreshThemeOnly();
+ canvas.repaint();
+ }
+ });
+ return item;
+ }
+
+ private JMenu createColorVisionMenu(final Preferences pref) {
+ JMenu menu = new JMenu("Color Vision");
+ ButtonGroup group = new ButtonGroup();
+ AccessibilityColorVisionDeficiency[] modes = {
+ AccessibilityColorVisionDeficiency.NONE,
+ AccessibilityColorVisionDeficiency.PROTANOPIA,
+ AccessibilityColorVisionDeficiency.DEUTERANOPIA,
+ AccessibilityColorVisionDeficiency.TRITANOPIA,
+ AccessibilityColorVisionDeficiency.MONOCHROMACY
+ };
+ String[] labels = {"None", "Protanopia", "Deuteranopia", "Tritanopia", "Monochromacy"};
+ for (int i = 0; i < modes.length; i++) {
+ final AccessibilityColorVisionDeficiency mode = modes[i];
+ JRadioButtonMenuItem item = new JRadioButtonMenuItem(labels[i], colorVisionDeficiency == mode);
+ item.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent event) {
+ colorVisionDeficiency = mode;
+ pref.put(PREF_ACCESSIBILITY_PREFIX + "colorVision", mode.name());
+ canvas.repaint();
+ }
+ });
+ group.add(item);
+ menu.add(item);
+ }
+ return menu;
+ }
+
+ /// Sets a simulated accessibility preference. This is primarily intended for
+ /// automated simulator tests. Preference indexes follow the menu order.
+ public void setSimulatorAccessibilityPreference(int preference, boolean enabled) {
+ switch (preference) {
+ case 0: highContrastEnabled = enabled; break;
+ case 1: differentiateWithoutColorEnabled = enabled; break;
+ case 2: reduceMotionEnabled = enabled; break;
+ case 3: reduceTransparencyEnabled = enabled; break;
+ case 4: boldTextEnabled = enabled; break;
+ case 5: invertColorsEnabled = enabled; break;
+ case 6: grayscaleEnabled = enabled; break;
+ case 7: onOffSwitchLabelsEnabled = enabled; break;
+ case 8: screenReaderEnabled = enabled; break;
+ default: throw new IllegalArgumentException("Unknown accessibility preference: " + preference);
+ }
+ }
+
+ /// Sets the simulator's color-vision filter.
+ public void setSimulatorColorVisionDeficiency(AccessibilityColorVisionDeficiency deficiency) {
+ colorVisionDeficiency = deficiency == null ? AccessibilityColorVisionDeficiency.NONE : deficiency;
+ if (canvas != null) {
+ canvas.repaint();
+ }
+ }
+
/// The Larger Text menu only changes the on-screen fonts when the running app
/// has opted into font scaling -- either through the `useLargerTextScaleBool`
/// theme constant or a `UIManager.setUseLargerTextScale(true)` call at startup.
@@ -8091,6 +8216,111 @@ public float getLargerTextScale() {
return largerTextScale;
}
+ @Override
+ public boolean isHighContrastEnabled() {
+ return highContrastEnabled;
+ }
+
+ @Override
+ public boolean isDifferentiateWithoutColorEnabled() {
+ return differentiateWithoutColorEnabled;
+ }
+
+ @Override
+ public AccessibilityColorVisionDeficiency getColorVisionDeficiency() {
+ return colorVisionDeficiency;
+ }
+
+ @Override
+ public boolean isReduceMotionEnabled() {
+ return reduceMotionEnabled;
+ }
+
+ @Override
+ public boolean isReduceTransparencyEnabled() {
+ return reduceTransparencyEnabled;
+ }
+
+ @Override
+ public boolean isBoldTextEnabled() {
+ return boldTextEnabled;
+ }
+
+ @Override
+ public boolean isInvertColorsEnabled() {
+ return invertColorsEnabled;
+ }
+
+ @Override
+ public boolean isGrayscaleEnabled() {
+ return grayscaleEnabled || colorVisionDeficiency == AccessibilityColorVisionDeficiency.MONOCHROMACY;
+ }
+
+ @Override
+ public boolean isOnOffSwitchLabelsEnabled() {
+ return onOffSwitchLabelsEnabled;
+ }
+
+ @Override
+ public boolean isScreenReaderEnabled() {
+ return screenReaderEnabled;
+ }
+
+ private BufferedImage accessibilityFilteredImage(BufferedImage source) {
+ boolean monochrome = grayscaleEnabled
+ || colorVisionDeficiency == AccessibilityColorVisionDeficiency.MONOCHROMACY;
+ if (!invertColorsEnabled && !monochrome
+ && (colorVisionDeficiency == AccessibilityColorVisionDeficiency.NONE
+ || colorVisionDeficiency == AccessibilityColorVisionDeficiency.UNKNOWN)) {
+ return source;
+ }
+ BufferedImage result = new BufferedImage(source.getWidth(), source.getHeight(), BufferedImage.TYPE_INT_ARGB);
+ int width = source.getWidth();
+ int height = source.getHeight();
+ int[] pixels = source.getRGB(0, 0, width, height, null, 0, width);
+ for (int i = 0; i < pixels.length; i++) {
+ int argb = pixels[i];
+ int transformed = transformAccessibilityColor(
+ (argb >>> 16) & 0xFF, (argb >>> 8) & 0xFF, argb & 0xFF, monochrome);
+ if (invertColorsEnabled) {
+ transformed = 0xFFFFFF ^ transformed;
+ }
+ pixels[i] = (argb & 0xFF000000) | transformed;
+ }
+ result.setRGB(0, 0, width, height, pixels, 0, width);
+ return result;
+ }
+
+ private int transformAccessibilityColor(int red, int green, int blue, boolean monochrome) {
+ if (monochrome) {
+ int gray = clampColor(0.299 * red + 0.587 * green + 0.114 * blue);
+ return (gray << 16) | (gray << 8) | gray;
+ }
+ switch (colorVisionDeficiency) {
+ case PROTANOPIA:
+ return colorMatrix(red, green, blue, .567, .433, 0, .558, .442, 0, 0, .242, .758);
+ case DEUTERANOPIA:
+ return colorMatrix(red, green, blue, .625, .375, 0, .7, .3, 0, 0, .3, .7);
+ case TRITANOPIA:
+ return colorMatrix(red, green, blue, .95, .05, 0, 0, .433, .567, 0, .475, .525);
+ default:
+ return (red << 16) | (green << 8) | blue;
+ }
+ }
+
+ private int colorMatrix(int red, int green, int blue,
+ double rr, double rg, double rb, double gr, double gg, double gb,
+ double br, double bg, double bb) {
+ int transformedRed = clampColor(rr * red + rg * green + rb * blue);
+ int transformedGreen = clampColor(gr * red + gg * green + gb * blue);
+ int transformedBlue = clampColor(br * red + bg * green + bb * blue);
+ return (transformedRed << 16) | (transformedGreen << 8) | transformedBlue;
+ }
+
+ private int clampColor(double value) {
+ return Math.max(0, Math.min(255, (int) Math.round(value)));
+ }
+
private void refreshSkin(final JFrame frm) {
Display.getInstance().callSerially(new Runnable() {
diff --git a/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/HTML5Implementation.java b/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/HTML5Implementation.java
index 9fb7ee00561..bab2b2bbb98 100644
--- a/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/HTML5Implementation.java
+++ b/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/HTML5Implementation.java
@@ -59,6 +59,7 @@
import com.codename1.teavm.jso.util.JSDateFormat;
import com.codename1.teavm.jso.util.JSNumberFormat;
import com.codename1.ui.Accessor;
+import com.codename1.ui.AccessibilityColorVisionDeficiency;
import com.codename1.ui.BrowserComponent;
import com.codename1.ui.BrowserWindow;
import com.codename1.ui.Button;
@@ -10810,6 +10811,35 @@ public boolean requiresHeavyButtonForCopyToClipboard() {
public Boolean isDarkMode() {
return isDarkMode_();
}
+
+ @JSBody(params={"query"}, script="return !!(window.matchMedia && window.matchMedia(query).matches);")
+ private static native boolean matchesMediaQuery(String query);
+
+ @Override
+ public boolean isHighContrastEnabled() {
+ return matchesMediaQuery("(forced-colors: active)")
+ || matchesMediaQuery("(prefers-contrast: more)");
+ }
+
+ @Override
+ public boolean isDifferentiateWithoutColorEnabled() {
+ return matchesMediaQuery("(forced-colors: active)");
+ }
+
+ @Override
+ public AccessibilityColorVisionDeficiency getColorVisionDeficiency() {
+ return AccessibilityColorVisionDeficiency.UNKNOWN;
+ }
+
+ @Override
+ public boolean isReduceMotionEnabled() {
+ return matchesMediaQuery("(prefers-reduced-motion: reduce)");
+ }
+
+ @Override
+ public boolean isReduceTransparencyEnabled() {
+ return matchesMediaQuery("(prefers-reduced-transparency: reduce)");
+ }
diff --git a/Ports/LinuxPort/src/com/codename1/impl/linux/LinuxImplementation.java b/Ports/LinuxPort/src/com/codename1/impl/linux/LinuxImplementation.java
index 53a4fc02dcb..9e50ad5f092 100644
--- a/Ports/LinuxPort/src/com/codename1/impl/linux/LinuxImplementation.java
+++ b/Ports/LinuxPort/src/com/codename1/impl/linux/LinuxImplementation.java
@@ -70,6 +70,23 @@
* headless (see {@code Ports/LinuxPort/status.md}).
*/
public class LinuxImplementation extends CodenameOneImplementation {
+
+ @Override
+ public boolean isHighContrastEnabled() {
+ String theme = System.getenv("GTK_THEME");
+ return theme != null && theme.toLowerCase().indexOf("highcontrast") >= 0;
+ }
+
+ @Override
+ public boolean isReduceMotionEnabled() {
+ return "0".equals(System.getenv("GTK_ENABLE_ANIMATIONS"));
+ }
+
+ @Override
+ public boolean isScreenReaderEnabled() {
+ String modules = System.getenv("GTK_MODULES");
+ return modules != null && (modules.indexOf("atk-bridge") >= 0 || modules.indexOf("gail") >= 0);
+ }
private static LinuxImplementation INSTANCE;
// Event type codes; must mirror the CN1EventType enum in cn1_linux.h.
diff --git a/Ports/WindowsPort/nativeSources/cn1_windows_accessibility.cpp b/Ports/WindowsPort/nativeSources/cn1_windows_accessibility.cpp
index 4dd96c2f93a..027faafd32d 100644
--- a/Ports/WindowsPort/nativeSources/cn1_windows_accessibility.cpp
+++ b/Ports/WindowsPort/nativeSources/cn1_windows_accessibility.cpp
@@ -1,24 +1,5 @@
/*
- * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
- * This code is free software; you can redistribute it and/or modify it
- * under the terms of the GNU General Public License version 2 only, as
- * published by the Free Software Foundation. Codename One designates this
- * particular file as subject to the "Classpath" exception as provided
- * by Oracle in the LICENSE file that accompanied this code.
- *
- * This code is distributed in the hope that it will be useful, but WITHOUT
- * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
- * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
- * version 2 for more details (a copy is included in the LICENSE file that
- * accompanied this code).
- *
- * You should have received a copy of the GNU General Public License version
- * 2 along with this work; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
- *
- * Please contact Codename One through http://www.codenameone.com/ if you
- * need additional information or have any questions.
+ * Copyright (c) 2026 Codename One and contributors. All rights reserved.
*/
/* Native virtual accessibility tree for the lightweight Win32 port. */
diff --git a/Ports/WindowsPort/nativeSources/cn1_windows_window.cpp b/Ports/WindowsPort/nativeSources/cn1_windows_window.cpp
index 94dd27557e2..3c851592b15 100644
--- a/Ports/WindowsPort/nativeSources/cn1_windows_window.cpp
+++ b/Ports/WindowsPort/nativeSources/cn1_windows_window.cpp
@@ -867,6 +867,29 @@ JAVA_BOOLEAN com_codename1_impl_windows_WindowsNative_isTouchDevice___R_boolean(
return JAVA_FALSE;
}
+JAVA_BOOLEAN com_codename1_impl_windows_WindowsNative_isHighContrastEnabled___R_boolean(CODENAME_ONE_THREAD_STATE) {
+ HIGHCONTRASTW contrast = {};
+ contrast.cbSize = sizeof(contrast);
+ if (!SystemParametersInfoW(SPI_GETHIGHCONTRAST, sizeof(contrast), &contrast, 0)) return JAVA_FALSE;
+ return (contrast.dwFlags & HCF_HIGHCONTRASTON) != 0 ? JAVA_TRUE : JAVA_FALSE;
+}
+
+JAVA_BOOLEAN com_codename1_impl_windows_WindowsNative_isReduceMotionEnabled___R_boolean(CODENAME_ONE_THREAD_STATE) {
+#ifdef SPI_GETCLIENTAREAANIMATION
+ BOOL animationsEnabled = TRUE;
+ if (!SystemParametersInfoW(SPI_GETCLIENTAREAANIMATION, 0, &animationsEnabled, 0)) return JAVA_FALSE;
+ return animationsEnabled ? JAVA_FALSE : JAVA_TRUE;
+#else
+ return JAVA_FALSE;
+#endif
+}
+
+JAVA_BOOLEAN com_codename1_impl_windows_WindowsNative_isScreenReaderEnabled___R_boolean(CODENAME_ONE_THREAD_STATE) {
+ BOOL screenReader = FALSE;
+ if (!SystemParametersInfoW(SPI_GETSCREENREADER, 0, &screenReader, 0)) return JAVA_FALSE;
+ return screenReader ? JAVA_TRUE : JAVA_FALSE;
+}
+
JAVA_INT com_codename1_impl_windows_WindowsNative_getDisplayHeight___R_int(CODENAME_ONE_THREAD_STATE) {
return cn1Win.height;
}
diff --git a/Ports/WindowsPort/src/com/codename1/impl/windows/WindowsImplementation.java b/Ports/WindowsPort/src/com/codename1/impl/windows/WindowsImplementation.java
index 763515be459..45ed71cc1c0 100644
--- a/Ports/WindowsPort/src/com/codename1/impl/windows/WindowsImplementation.java
+++ b/Ports/WindowsPort/src/com/codename1/impl/windows/WindowsImplementation.java
@@ -61,6 +61,21 @@
* minimal in this first cut and grow in later phases.
*/
public class WindowsImplementation extends CodenameOneImplementation {
+
+ @Override
+ public boolean isHighContrastEnabled() {
+ return WindowsNative.isHighContrastEnabled();
+ }
+
+ @Override
+ public boolean isReduceMotionEnabled() {
+ return WindowsNative.isReduceMotionEnabled();
+ }
+
+ @Override
+ public boolean isScreenReaderEnabled() {
+ return WindowsNative.isScreenReaderEnabled();
+ }
private static WindowsImplementation INSTANCE;
// Event type codes; must mirror the CN1EventType enum in cn1_windows.h.
diff --git a/Ports/WindowsPort/src/com/codename1/impl/windows/WindowsNative.java b/Ports/WindowsPort/src/com/codename1/impl/windows/WindowsNative.java
index 8d6d1d041e9..24f363c83d3 100644
--- a/Ports/WindowsPort/src/com/codename1/impl/windows/WindowsNative.java
+++ b/Ports/WindowsPort/src/com/codename1/impl/windows/WindowsNative.java
@@ -156,6 +156,12 @@ public static native void accessibilityNode(long id, long parentId, String role,
*/
public static native boolean isTouchDevice();
+ public static native boolean isHighContrastEnabled();
+
+ public static native boolean isReduceMotionEnabled();
+
+ public static native boolean isScreenReaderEnabled();
+
/**
* Parks the calling (main) thread to keep the process alive in headless
* screenshot mode, where there is no window message loop. The EDT exits the
diff --git a/Ports/iOSPort/nativeSources/IOSNative.m b/Ports/iOSPort/nativeSources/IOSNative.m
index 90a482a0573..2e5f2c7a8cf 100644
--- a/Ports/iOSPort/nativeSources/IOSNative.m
+++ b/Ports/iOSPort/nativeSources/IOSNative.m
@@ -5754,6 +5754,48 @@ JAVA_FLOAT com_codename1_impl_ios_IOSNative_getLargerTextScale___R_float(CN1_THR
#endif // !TARGET_OS_WATCH && !TARGET_OS_TV
}
+JAVA_BOOLEAN com_codename1_impl_ios_IOSNative_isHighContrastEnabled___R_boolean(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT instanceObject) {
+ return UIAccessibilityDarkerSystemColorsEnabled() ? 1 : 0;
+}
+
+JAVA_BOOLEAN com_codename1_impl_ios_IOSNative_isDifferentiateWithoutColorEnabled___R_boolean(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT instanceObject) {
+ if (@available(iOS 13.0, macCatalyst 13.1, *)) {
+ return UIAccessibilityShouldDifferentiateWithoutColor() ? 1 : 0;
+ }
+ return 0;
+}
+
+JAVA_BOOLEAN com_codename1_impl_ios_IOSNative_isReduceMotionEnabled___R_boolean(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT instanceObject) {
+ return UIAccessibilityIsReduceMotionEnabled() ? 1 : 0;
+}
+
+JAVA_BOOLEAN com_codename1_impl_ios_IOSNative_isReduceTransparencyEnabled___R_boolean(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT instanceObject) {
+ return UIAccessibilityIsReduceTransparencyEnabled() ? 1 : 0;
+}
+
+JAVA_BOOLEAN com_codename1_impl_ios_IOSNative_isBoldTextEnabled___R_boolean(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT instanceObject) {
+ return UIAccessibilityIsBoldTextEnabled() ? 1 : 0;
+}
+
+JAVA_BOOLEAN com_codename1_impl_ios_IOSNative_isInvertColorsEnabled___R_boolean(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT instanceObject) {
+ return UIAccessibilityIsInvertColorsEnabled() ? 1 : 0;
+}
+
+JAVA_BOOLEAN com_codename1_impl_ios_IOSNative_isGrayscaleEnabled___R_boolean(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT instanceObject) {
+ return UIAccessibilityIsGrayscaleEnabled() ? 1 : 0;
+}
+
+JAVA_BOOLEAN com_codename1_impl_ios_IOSNative_isOnOffSwitchLabelsEnabled___R_boolean(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT instanceObject) {
+ if (@available(iOS 13.0, macCatalyst 13.1, *)) {
+ return UIAccessibilityIsOnOffSwitchLabelsEnabled() ? 1 : 0;
+ }
+ return 0;
+}
+
+JAVA_BOOLEAN com_codename1_impl_ios_IOSNative_isScreenReaderEnabled___R_boolean(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT instanceObject) {
+ return UIAccessibilityIsVoiceOverRunning() ? 1 : 0;
+}
+
#ifdef INCLUDE_LOCATION_USAGE
CLLocationManager* com_codename1_impl_ios_IOSNative_createCLLocation = nil;
#endif
diff --git a/Ports/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java b/Ports/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java
index 5f1454230bd..00187319f2b 100644
--- a/Ports/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java
+++ b/Ports/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java
@@ -1652,6 +1652,51 @@ public boolean isLargerTextEnabled() {
public float getLargerTextScale() {
return nativeInstance.getLargerTextScale();
}
+
+ @Override
+ public boolean isHighContrastEnabled() {
+ return nativeInstance.isHighContrastEnabled();
+ }
+
+ @Override
+ public boolean isDifferentiateWithoutColorEnabled() {
+ return nativeInstance.isDifferentiateWithoutColorEnabled();
+ }
+
+ @Override
+ public boolean isReduceMotionEnabled() {
+ return nativeInstance.isReduceMotionEnabled();
+ }
+
+ @Override
+ public boolean isReduceTransparencyEnabled() {
+ return nativeInstance.isReduceTransparencyEnabled();
+ }
+
+ @Override
+ public boolean isBoldTextEnabled() {
+ return nativeInstance.isBoldTextEnabled();
+ }
+
+ @Override
+ public boolean isInvertColorsEnabled() {
+ return nativeInstance.isInvertColorsEnabled();
+ }
+
+ @Override
+ public boolean isGrayscaleEnabled() {
+ return nativeInstance.isGrayscaleEnabled();
+ }
+
+ @Override
+ public boolean isOnOffSwitchLabelsEnabled() {
+ return nativeInstance.isOnOffSwitchLabelsEnabled();
+ }
+
+ @Override
+ public boolean isScreenReaderEnabled() {
+ return nativeInstance.isScreenReaderEnabled();
+ }
public void flushGraphics() {
diff --git a/Ports/iOSPort/src/com/codename1/impl/ios/IOSNative.java b/Ports/iOSPort/src/com/codename1/impl/ios/IOSNative.java
index c935634bd14..ffe5f3b8f14 100644
--- a/Ports/iOSPort/src/com/codename1/impl/ios/IOSNative.java
+++ b/Ports/iOSPort/src/com/codename1/impl/ios/IOSNative.java
@@ -191,6 +191,15 @@ native void fillGradient(int kind, int stopCount, float[] positions, float[] pre
native boolean isLargerTextEnabled();
native float getLargerTextScale();
+ native boolean isHighContrastEnabled();
+ native boolean isDifferentiateWithoutColorEnabled();
+ native boolean isReduceMotionEnabled();
+ native boolean isReduceTransparencyEnabled();
+ native boolean isBoldTextEnabled();
+ native boolean isInvertColorsEnabled();
+ native boolean isGrayscaleEnabled();
+ native boolean isOnOffSwitchLabelsEnabled();
+ native boolean isScreenReaderEnabled();
// SJH Nov. 17, 2015 : Removing native isMinimized() method because it conflicted with
// tracking on the java side. It caused the app to still be minimized inside start()
diff --git a/docs/demos/common/src/main/java/com/codenameone/developerguide/snippets/AccessibilitySemanticsSnippets.java b/docs/demos/common/src/main/java/com/codenameone/developerguide/snippets/AccessibilitySemanticsSnippets.java
index f86cd8e3f88..e24682da8be 100644
--- a/docs/demos/common/src/main/java/com/codenameone/developerguide/snippets/AccessibilitySemanticsSnippets.java
+++ b/docs/demos/common/src/main/java/com/codenameone/developerguide/snippets/AccessibilitySemanticsSnippets.java
@@ -1,28 +1,11 @@
/*
- * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
- * This code is free software; you can redistribute it and/or modify it
- * under the terms of the GNU General Public License version 2 only, as
- * published by the Free Software Foundation. Codename One designates this
- * particular file as subject to the "Classpath" exception as provided
- * by Oracle in the LICENSE file that accompanied this code.
- *
- * This code is distributed in the hope that it will be useful, but WITHOUT
- * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
- * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
- * version 2 for more details (a copy is included in the LICENSE file that
- * accompanied this code).
- *
- * You should have received a copy of the GNU General Public License version
- * 2 along with this work; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
- *
- * Please contact Codename One through http://www.codenameone.com/ if you
- * need additional information or have any questions.
+ * Copyright (c) 2026 Codename One and contributors. All rights reserved.
*/
package com.codenameone.developerguide.snippets;
import com.codename1.ui.Button;
+import com.codename1.ui.AccessibilityColorVisionDeficiency;
+import com.codename1.ui.CN;
import com.codename1.ui.Component;
import com.codename1.ui.Form;
import com.codename1.ui.Label;
@@ -165,9 +148,65 @@ public String inspect(Form myForm, int screenX, int screenY) {
return diagnosticJson + (hit == null ? "" : hit.getLabel());
}
+ public void adaptToAccessibilityPreferences(Component animatedChart, Label connectionStatus) {
+ // tag::accessibility-preferences[]
+ if (CN.isHighContrastEnabled()) {
+ animatedChart.setUIID("HighContrastChart");
+ }
+
+ // Important state always uses text or an icon as well as color. The
+ // preference is an extra signal, not permission to use color alone.
+ if (CN.isDifferentiateWithoutColorEnabled()
+ || CN.getColorVisionDeficiency() != AccessibilityColorVisionDeficiency.NONE) {
+ connectionStatus.setText("Disconnected: action required");
+ }
+
+ if (CN.isReduceMotionEnabled()) {
+ animatedChart.putClientProperty("animate", Boolean.FALSE);
+ }
+ if (CN.isReduceTransparencyEnabled()) {
+ animatedChart.setUIID("OpaqueChart");
+ }
+ // end::accessibility-preferences[]
+ }
+
+ public void repairCustomControl(Component iconOnlyButton) {
+ // tag::accessibility-repair[]
+ iconOnlyButton.getSemantics()
+ .setRole(AccessibilityRole.BUTTON)
+ .setLabel("Delete message")
+ .setHint("Removes this message from the conversation")
+ .setIdentifier("message-delete")
+ .addAction(new AccessibilityAction(
+ AccessibilityAction.ACTIVATE,
+ null,
+ new AccessibilityAction.Handler() {
+ public boolean perform(Component component, Object argument) {
+ deleteMessage();
+ return true;
+ }
+ }));
+ // end::accessibility-repair[]
+ }
+
+ public void assertAccessibleForm(Form form) {
+ // tag::accessibility-regression-assertions[]
+ AccessibilityTreeSnapshot tree = AccessibilityInspector.snapshot(form);
+ AccessibilityAssertions.assertNoErrors(tree);
+ AccessibilityAssertions.assertNoUnlabeledInteractiveNodes(tree);
+
+ AccessibilityNodeSnapshot save = tree.getNodeByIdentifier("profile-save");
+ assert save.getRole() == AccessibilityRole.BUTTON;
+ assert save.getAction(AccessibilityAction.ACTIVATE) != null;
+ // end::accessibility-regression-assertions[]
+ }
+
private void archiveMessage() {
}
+ private void deleteMessage() {
+ }
+
/** Minimal model contract used by the virtual-descendant example. */
public interface ChartPoint {
String getId();
diff --git a/docs/developer-guide/Accessibility-Semantics.asciidoc b/docs/developer-guide/Accessibility-Semantics.asciidoc
index ca3bb43edd5..b2fb44b2da8 100644
--- a/docs/developer-guide/Accessibility-Semantics.asciidoc
+++ b/docs/developer-guide/Accessibility-Semantics.asciidoc
@@ -83,6 +83,53 @@ include::../demos/common/src/main/java/com/codenameone/developerguide/snippets/A
Form changes and nodes with a pane title produce screen or pane transition notifications. `Display.announceForAccessibility()` remains available for an exceptional announcement that isn't represented by persistent UI. Prefer a live region for state that's visible on screen.
+== User accessibility preferences
+
+Semantics describe what a control means. Accessibility preferences describe how the user wants the interface presented. Read these preferences through `Display` or the matching static shortcuts in `CN`:
+
+[cols="2,3"]
+|===
+|API |Meaning
+
+|`isHighContrastEnabled()`
+|The user requests stronger foreground and background contrast.
+
+|`isDifferentiateWithoutColorEnabled()`
+|The user requests labels, shapes, or patterns in addition to color.
+
+|`getColorVisionDeficiency()`
+|Returns `NONE`, `PROTANOPIA`, `DEUTERANOPIA`, `TRITANOPIA`, `MONOCHROMACY`, or `UNKNOWN`.
+
+|`isReduceMotionEnabled()`
+|Nonessential motion should be removed or replaced with an instant or cross-fade change.
+
+|`isReduceTransparencyEnabled()`
+|Blur and translucent surfaces should use an opaque alternative.
+
+|`isBoldTextEnabled()`
+|The system requests a heavier text weight.
+
+|`isInvertColorsEnabled()` and `isGrayscaleEnabled()`
+|The operating system is transforming the displayed colors.
+
+|`isOnOffSwitchLabelsEnabled()`
+|Switches should include a visible on/off distinction.
+
+|`isScreenReaderEnabled()`
+|A screen reader or touch-exploration service is active.
+|===
+
+An unavailable preference returns `false`; an unavailable color-vision mode returns `UNKNOWN`. Don't hide accessibility support behind `isScreenReaderEnabled()`. A screen reader can start after the application launches, and other assistive technologies consume the same semantic tree.
+
+[source,java]
+----
+include::../demos/common/src/main/java/com/codenameone/developerguide/snippets/AccessibilitySemanticsSnippets.java[tag=accessibility-preferences,indent=0]
+----
+
+IMPORTANT: Never use `getColorVisionDeficiency() == NONE` as permission to convey information only through color. The setting can be unavailable, a person might not enable a system filter, and contrast can change with lighting and display hardware. Design the default interface with redundant cues, then use preferences for further adaptation.
+
+Preference detection uses each platform's public signals. iOS and Mac Catalyst expose contrast, differentiate-without-color, motion, transparency, bold text, inversion, grayscale, switch labels, and VoiceOver; Apple doesn't expose the selected color-filter type, so the deficiency value is `UNKNOWN`. Android reads high-text contrast, display inversion and daltonizer mode, animation scale, font-weight adjustment, and touch exploration. Windows reads High Contrast, client-area animation, and screen-reader settings. Linux detects the GTK HighContrast theme, disabled GTK animation, and ATK bridge activation. JavaScript uses `forced-colors`, `prefers-contrast`, `prefers-reduced-motion`, and `prefers-reduced-transparency`. The Java SE simulator provides deterministic controls for every portable signal.
+
== Collections
`AccessibilityCollectionInfo` describes row and column counts, hierarchy, and selection mode. `AccessibilityCollectionItemInfo` describes row and column index, spans, position and size in a set, nesting level, and header status.
@@ -103,6 +150,48 @@ include::../demos/common/src/main/java/com/codenameone/developerguide/snippets/A
The Java SE Component Inspector provides *Copy Accessibility Tree JSON* and *Audit Accessibility Tree* actions. Tests should assert resolved semantic properties instead of platform-specific spoken sentences. The `scripts/hellocodenameone` suite contains the cross-port conformance fixture used by native CI.
+== Diagnosing and correcting an inaccessible screen
+
+Accessibility review works best as a repeatable loop instead of a final checklist:
+
+. Use the keyboard to reach every operation. Verify visible focus, logical order, escape from modal UI, and operation without pointer gestures.
+. Open *Tools → Component Inspector*. Select the form or a suspicious component, then choose *Audit Accessibility Tree*. Start with errors, such as an unlabeled interactive node, invalid range, or unnamed dialog.
+. Choose *Copy Accessibility Tree JSON* and inspect the resolved role, label, value, state, actions, bounds, and child order. This catches cases where the visual component hierarchy doesn't match the virtual accessibility hierarchy.
+. Use *Simulate → Accessibility Preferences* to test high contrast, reduced motion, reduced transparency, inversion, grayscale, and each color-vision filter. Also test *Simulate → Larger Text* at Accessibility 5. Look for clipped text, color-only state, invisible focus, unreadable overlays, and motion that remains essential to understanding the result.
+. Correct the component API or theme. Run the audit again, add a focused assertion, and verify the native screen reader before considering the issue closed.
+
+The simulator menu changes the values returned by `Display` and `CN`. Color-vision, inversion, and grayscale choices also filter the simulator canvas, so screenshots show approximately what remains distinguishable. These filters are diagnostic approximations; they don't replace testing with platform display filters or users.
+
+.Accessibility preferences in the Java SE simulator
+image::accessibility-simulator-preferences.png[Java SE simulator with the Accessibility Preferences menu open,scaledwidth=80%]
+
+The following icon-only custom control is painted and responds to a pointer, but initially has no usable name, role, or keyboard-equivalent action. The audit reports it as an unlabeled interactive node. Repair the semantic contract at the custom-control boundary:
+
+[source,java]
+----
+include::../demos/common/src/main/java/com/codenameone/developerguide/snippets/AccessibilitySemanticsSnippets.java[tag=accessibility-repair,indent=0]
+----
+
+Don't fix the warning by adding a label alone if the role, current state, range, or available actions are still wrong. For example, a custom switch needs `SWITCH`, checked state, and activation; a chart needs virtual descendants for its meaningful data points; a renderer-backed grid needs collection and item metadata.
+
+.Auditing the semantic tree in the Component Inspector
+image::accessibility-component-inspector-audit.png[Component Inspector showing an accessibility audit,scaledwidth=80%]
+
+=== Assertions that prevent regressions
+
+Snapshot the finished form and assert behavior that matters to a user. Prefer stable identifiers and resolved semantics over implementation class names or platform-specific speech:
+
+[source,java]
+----
+include::../demos/common/src/main/java/com/codenameone/developerguide/snippets/AccessibilitySemanticsSnippets.java[tag=accessibility-regression-assertions,indent=0]
+----
+
+Keep targeted assertions for traversal order, validation errors, live-region behavior, ranges, and stable virtual keys. The `scripts/hellocodenameone` accessibility fixture invokes every portable preference API and exercises the resolved semantic tree on each native CI platform.
+
+=== Native verification
+
+The simulator finds portable problems, but the final pass must use VoiceOver, TalkBack, Narrator, Orca, or the browser accessibility tree. Navigate in both directions, activate every action, change adjustable values, enter and leave collections, trigger errors, and confirm that focus moves sensibly after a dialog, navigation, deletion, or live update. Record the platform, assistive-technology version, input method, and failed semantic node in the bug report; attach the inspector JSON when the native result differs from the portable snapshot.
+
== Platform mappings
[cols="1,2,2"]
diff --git a/docs/developer-guide/img/accessibility-component-inspector-audit.png b/docs/developer-guide/img/accessibility-component-inspector-audit.png
new file mode 100644
index 0000000000000000000000000000000000000000..923240ed932125f710d35c1c27bf50636a742a59
GIT binary patch
literal 237174
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