Skip to content

Commit 7d76862

Browse files
authored
Fix VoiceOver clicking the wrong button and refactor accessibility implementation (JetBrains#2720)
1 parent a96cc35 commit 7d76862

12 files changed

Lines changed: 509 additions & 538 deletions

compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeWindow.desktop.kt

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ import java.awt.event.MouseListener
3636
import java.awt.event.MouseMotionListener
3737
import java.awt.event.MouseWheelListener
3838
import java.util.Locale
39-
import javax.accessibility.Accessible
4039
import javax.swing.JFrame
4140
import org.jetbrains.skiko.GraphicsApi
4241
import org.jetbrains.skiko.SkiaLayerAnalytics
@@ -79,10 +78,6 @@ class ComposeWindow @ExperimentalComposeUiApi constructor(
7978
internal val windowContext by composePanel::windowContext
8079
internal var rootForTestListener by composePanel::rootForTestListener
8180

82-
// Don't override the accessible context of JFrame, since accessibility work through HardwareLayer
83-
internal val windowAccessible: Accessible
84-
get() = composePanel.windowAccessible
85-
8681
/**
8782
* Returns the [SemanticsOwner]s corresponding to the roots of the semantics trees in this
8883
* [ComposeWindow].

compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeWindowPanel.desktop.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,6 @@ internal class ComposeWindowPanel(
7474
}
7575
private val contentComponent by composeContainer::contentComponent
7676

77-
val windowAccessible by composeContainer::accessible
7877
val windowContext by composeContainer::windowContext
7978
var rootForTestListener by composeContainer::rootForTestListener
8079
var fullscreen by composeContainer::fullscreen

compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/a11y/Accessibility.desktop.kt

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,13 @@ import org.jetbrains.skiko.initializeCAccessible
1717
*/
1818
internal class AccessibleFocusHelper(
1919
private val component: Component,
20-
private val regularAccessible: Accessible,
20+
private val sceneAccessibleContext: AccessibleContext,
2121
) {
2222

2323
private var focusedAccessible: Accessible? = null
2424

2525
val accessibleContext: AccessibleContext
26-
get() = (focusedAccessible ?: regularAccessible).accessibleContext
26+
get() = focusedAccessible?.accessibleContext ?: sceneAccessibleContext
2727

2828
private var resetFocusAccessibleJob: Job? = null
2929

@@ -44,9 +44,24 @@ internal class AccessibleFocusHelper(
4444
// and its accessibility context. This timeout is used to deal with concurrency
4545
// TODO Find more reliable procedure
4646
resetFocusAccessibleJob?.cancel()
47-
resetFocusAccessibleJob = GlobalScope.launch(MainUIDispatcher) {
48-
delay(100)
49-
focusedAccessible = null
47+
if (focusedAccessible != null) {
48+
resetFocusAccessibleJob = GlobalScope.launch(MainUIDispatcher) {
49+
delay(100)
50+
focusedAccessible = null
51+
}
52+
}
53+
}
54+
55+
/**
56+
* When [focusedAccessible] is set, for the accessible hierarchy to be correct, its parent must
57+
* be reported as the scene's accessible context. This function returns it if [accessible] is
58+
* [focusedAccessible].
59+
*/
60+
fun accessibleParentOverride(accessible: Accessible): Accessible? {
61+
return if (accessible == focusedAccessible) {
62+
sceneAccessibleContext.accessibleParent as Accessible
63+
} else {
64+
null
5065
}
5166
}
5267

compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/a11y/ComposeAccessible.kt

Lines changed: 39 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ private typealias ActionKey = SemanticsPropertyKey<AccessibilityAction<() -> Boo
7575
*/
7676
internal class ComposeAccessible(
7777
semanticsNode: SemanticsNode,
78-
private val controller: AccessibilityController
78+
private val ownerAccessibility: SemanticsOwnerAccessibility
7979
) : Accessible,
8080
// Must be a subclass of java.awt.Component because CAccessible only registers property
8181
// listeners with the accessible context if the Accessible is an instance of java.awt.Component
@@ -171,7 +171,7 @@ internal class ComposeAccessible(
171171
get() = semanticsConfig.getOrNull(SemanticsProperties.Selected)
172172

173173
private val density: Density
174-
get() = controller.desktopComponent.density
174+
get() = ownerAccessibility.desktopComponent.density
175175

176176
val horizontalScroll
177177
get() = semanticsConfig.getOrNull(SemanticsProperties.HorizontalScrollAxisRange)
@@ -266,60 +266,61 @@ internal class ComposeAccessible(
266266
}
267267

268268
override fun getAccessibleParent(): Accessible? {
269-
val parentNode = semanticsNode.parent ?: return controller.parentAccessible
270-
return controller.accessibleByNodeId(parentNode.id)!!
269+
return ownerAccessibility.accessibleParentOf(this@ComposeAccessible)
271270
}
272271

273272
override fun getAccessibleIndexInParent(): Int {
274-
val parent = semanticsNode.parent ?: return controller.indexInScene()
273+
val parent = semanticsNode.parent ?: return ownerAccessibility.indexInScene()
275274
return parent.traversalOrderedChildren().indexOfFirst { it.id == semanticsNode.id }
276275
}
277276

278277
override fun getAccessibleComponent(): AccessibleComponent? {
279278
return this
280279
}
281280

282-
// we have to store a reference to AccessibleAction, because AWT itself uses weak
283-
// references and GC could delete an object which is, in fact, in use
284-
private var accessibleAction: AccessibleAction? = null
281+
private var accessibleActions: List<Pair<String, ActionKey>>? = null
285282

286-
override fun getAccessibleAction(): AccessibleAction? {
287-
val actions = mutableListOf<Pair<String?, ActionKey>>()
283+
private fun updateAccessibleActions() {
284+
val actions = mutableListOf<Pair<String, ActionKey>>()
288285

289-
fun addActionIfExist(key: SemanticsPropertyKey<AccessibilityAction<() -> Boolean>>) {
290-
semanticsConfig.getOrNull(key)?.let {
291-
actions.add(Pair(it.label, key))
292-
}
293-
}
294-
semanticsConfig.getOrNull(SemanticsActions.OnClick)?.let {
295-
// AWT expects "click" label for click actions, at least on macOS...
296-
actions.add(Pair("click", SemanticsActions.OnClick))
286+
fun addActionIfExist(
287+
key: SemanticsPropertyKey<AccessibilityAction<() -> Boolean>>,
288+
label: String? = null,
289+
) {
290+
val action = semanticsConfig.getOrNull(key) ?: return
291+
val label = label ?: action.label ?: return // No point to add an action without a label
292+
actions.add(Pair(label, key))
297293
}
298294

295+
// AWT expects a "click" label for click actions, at least on macOS...
296+
addActionIfExist(SemanticsActions.OnClick, AccessibleAction.CLICK)
299297
addActionIfExist(SemanticsActions.OnLongClick)
300298
addActionIfExist(SemanticsActions.Expand)
301299
addActionIfExist(SemanticsActions.Collapse)
302300
addActionIfExist(SemanticsActions.Dismiss)
303301

304-
if (actions.isEmpty()) {
305-
return null
306-
}
307-
accessibleAction = object : AccessibleAction {
308-
override fun getAccessibleActionCount(): Int = actions.size
302+
accessibleActions = actions.takeIf { it.isNotEmpty() }
303+
}
309304

310-
override fun getAccessibleActionDescription(i: Int): String? {
311-
val (label, _) = actions[i]
312-
return label
313-
}
305+
override fun getAccessibleAction(): AccessibleAction? {
306+
updateAccessibleActions()
307+
return if (accessibleActions == null) null else this
308+
}
314309

315-
override fun doAccessibleAction(i: Int): Boolean {
316-
val (_, actionKey) = actions[i]
317-
return semanticsConfig.getOrNull(actionKey)?.let {
318-
it.action?.invoke()
319-
} ?: false
320-
}
321-
}
322-
return accessibleAction
310+
override fun getAccessibleActionCount(): Int = accessibleActions?.size ?: 0
311+
312+
override fun getAccessibleActionDescription(i: Int): String? {
313+
val actions = accessibleActions!!
314+
val (label, _) = actions[i]
315+
return label
316+
}
317+
318+
override fun doAccessibleAction(i: Int): Boolean {
319+
val actions = accessibleActions!!
320+
val (_, actionKey) = actions[i]
321+
return semanticsConfig.getOrNull(actionKey)?.let {
322+
it.action?.invoke()
323+
} ?: false
323324
}
324325

325326
override fun getAccessibleValue(): AccessibleValue? {
@@ -341,7 +342,7 @@ internal class ComposeAccessible(
341342
val regularChildren = semanticsNode.traversalOrderedChildren()
342343
val childrenSize = regularChildren.size
343344
return if (index < childrenSize) {
344-
controller.accessibleByNodeId(regularChildren[index].id)
345+
ownerAccessibility.accessibleByNodeId(regularChildren[index].id)
345346
} else {
346347
auxiliaryChildren[index - childrenSize]
347348
}
@@ -381,7 +382,7 @@ internal class ComposeAccessible(
381382
override fun getAccessibleAt(p: Point): Accessible? {
382383
val accessibleChildren = semanticsNode.traversalOrderedChildren()
383384
for (child in accessibleChildren) {
384-
val accessible = controller.accessibleByNodeId(child.id) as? Accessible ?: continue
385+
val accessible = ownerAccessibility.accessibleByNodeId(child.id) as? Accessible ?: continue
385386
val accessibleComponent = (accessible.accessibleContext as? AccessibleComponent) ?: continue
386387
accessibleComponent.getAccessibleAt(p)?.let {
387388
return it
@@ -464,7 +465,7 @@ internal class ComposeAccessible(
464465
}
465466

466467
override fun getAccessibleRole(): AccessibleRole {
467-
AccessibilityController.AccessibilityUsage.notifyInUse()
468+
SemanticsOwnerAccessibility.AccessibilityUsage.notifyInUse()
468469
return computeAccessibleRole()
469470
}
470471

@@ -884,20 +885,6 @@ internal class ComposeAccessible(
884885
TODO("Not yet implemented")
885886
}
886887

887-
// For some reasons JDK's CAccessibility does not call getAccessibleAction
888-
// and performs actions on current component itself
889-
override fun getAccessibleActionCount(): Int {
890-
return accessibleAction?.accessibleActionCount ?: 0
891-
}
892-
893-
override fun getAccessibleActionDescription(i: Int): String {
894-
return accessibleAction?.getAccessibleActionDescription(i) ?: ""
895-
}
896-
897-
override fun doAccessibleAction(i: Int): Boolean {
898-
return accessibleAction?.doAccessibleAction(i) ?: false
899-
}
900-
901888
private fun List<CharSequence>.mergeText() = joinToString(", ")
902889
}
903890
}

0 commit comments

Comments
 (0)