Skip to content

Commit dd52aac

Browse files
committed
Remove ComposeSceneAccessible and refactor accessibility implementation
1 parent 8a1f7cd commit dd52aac

File tree

12 files changed

+397
-401
lines changed

12 files changed

+397
-401
lines changed

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: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,18 @@ import org.jetbrains.skiko.initializeCAccessible
1717
*/
1818
internal class AccessibleFocusHelper(
1919
private val component: Component,
20-
private val regularAccessible: Accessible,
20+
private val regularAccessibleContext: AccessibleContext,
2121
) {
2222

2323
private var focusedAccessible: Accessible? = null
2424

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

2828
private var resetFocusAccessibleJob: Job? = null
2929

3030
@OptIn(DelicateCoroutinesApi::class)
31-
fun requestFocusOnAccessible(accessible: Accessible?) {
31+
fun requestFocusOnAccessible(accessible: Accessible) {
3232
focusedAccessible = accessible
3333

3434
when (hostOs) {
@@ -50,6 +50,19 @@ internal class AccessibleFocusHelper(
5050
}
5151
}
5252

53+
/**
54+
* When [focusedAccessible] is set, for the accessible hierarchy to be correct, its parent must
55+
* be reported as the scene's accessible context. This function returns it if [accessible] is
56+
* [focusedAccessible].
57+
*/
58+
fun accessibleParentOverride(accessible: Accessible): Accessible? {
59+
return if (accessible == focusedAccessible) {
60+
regularAccessibleContext.accessibleParent as Accessible
61+
} else {
62+
null
63+
}
64+
}
65+
5366
private fun requestAccessBridgeFocusOnAccessible() {
5467
val focusEvent = FocusEvent(component, FocusEvent.FOCUS_GAINED)
5568
component.focusListeners.forEach { it.focusGained(focusEvent) }

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

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package androidx.compose.ui.platform.a11y
1818

1919
import androidx.collection.mutableScatterMapOf
2020
import androidx.compose.ui.platform.PlatformComponent
21+
import androidx.compose.ui.platform.a11y.ComposeSceneAccessibility.ComposeSceneAccessibleContext
2122
import androidx.compose.ui.semantics.ProgressBarRangeInfo
2223
import androidx.compose.ui.semantics.SemanticsConfiguration
2324
import androidx.compose.ui.semantics.SemanticsNode
@@ -50,13 +51,13 @@ import kotlinx.coroutines.launch
5051
* when a [SemanticsNode] from [owner] received a focus. `null` is passed when no nodes in the scene
5152
* are focused.
5253
*
53-
* @see ComposeSceneAccessible
54+
* @see ComposeSceneAccessibleContext
5455
* @see ComposeAccessible
5556
*/
5657
internal class AccessibilityController(
5758
val owner: SemanticsOwner,
5859
val desktopComponent: PlatformComponent,
59-
val parentAccessible: ComposeSceneAccessible,
60+
val sceneAccessibility: ComposeSceneAccessibility,
6061
private val onFocusReceived: (ComposeAccessible?) -> Unit,
6162
) {
6263

@@ -92,7 +93,22 @@ internal class AccessibilityController(
9293
* Returns the index of this [AccessibilityController]'s root node in the scene.
9394
*/
9495
fun indexInScene(): Int {
95-
return parentAccessible.indexOfChild(this)
96+
return sceneAccessibility.indexOfChild(this)
97+
}
98+
99+
/**
100+
* Returns the scene's [Accessible].
101+
*/
102+
fun sceneAccessible(): Accessible? {
103+
return sceneAccessibility.accessible()
104+
}
105+
106+
/**
107+
* Returns an override of the given [Accessible]'s parent; `null` if there is no override and
108+
* its parent should be reported as its normal parent from the node hierarchy.
109+
*/
110+
fun accessibleParentOverride(accessible: ComposeAccessible): Accessible? {
111+
return sceneAccessibility.accessibleParentOverride(accessible)
96112
}
97113

98114
/**

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

Lines changed: 35 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -266,7 +266,9 @@ internal class ComposeAccessible(
266266
}
267267

268268
override fun getAccessibleParent(): Accessible? {
269-
val parentNode = semanticsNode.parent ?: return controller.parentAccessible
269+
controller.accessibleParentOverride(this@ComposeAccessible)?.let { return it }
270+
271+
val parentNode = semanticsNode.parent ?: return controller.sceneAccessible()
270272
return controller.accessibleByNodeId(parentNode.id)!!
271273
}
272274

@@ -279,47 +281,49 @@ internal class ComposeAccessible(
279281
return this
280282
}
281283

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
284+
private var accessibleActions: List<Pair<String, ActionKey>>? = null
285285

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

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))
289+
fun addActionIfExist(
290+
key: SemanticsPropertyKey<AccessibilityAction<() -> Boolean>>,
291+
label: String? = null,
292+
) {
293+
val action = semanticsConfig.getOrNull(key) ?: return
294+
val label = label ?: action.label ?: return // No point to add an action without a label
295+
actions.add(Pair(label, key))
297296
}
298297

298+
// AWT expects a "click" label for click actions, at least on macOS...
299+
addActionIfExist(SemanticsActions.OnClick, AccessibleAction.CLICK)
299300
addActionIfExist(SemanticsActions.OnLongClick)
300301
addActionIfExist(SemanticsActions.Expand)
301302
addActionIfExist(SemanticsActions.Collapse)
302303
addActionIfExist(SemanticsActions.Dismiss)
303304

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

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

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
313+
override fun getAccessibleActionCount(): Int = accessibleActions?.size ?: 0
314+
315+
override fun getAccessibleActionDescription(i: Int): String? {
316+
val actions = accessibleActions!!
317+
val (label, _) = actions[i]
318+
return label
319+
}
320+
321+
override fun doAccessibleAction(i: Int): Boolean {
322+
val actions = accessibleActions!!
323+
val (_, actionKey) = actions[i]
324+
return semanticsConfig.getOrNull(actionKey)?.let {
325+
it.action?.invoke()
326+
} ?: false
323327
}
324328

325329
override fun getAccessibleValue(): AccessibleValue? {
@@ -884,20 +888,6 @@ internal class ComposeAccessible(
884888
TODO("Not yet implemented")
885889
}
886890

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-
901891
private fun List<CharSequence>.mergeText() = joinToString(", ")
902892
}
903893
}

compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/a11y/ComposeSceneAccessible.kt renamed to compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/a11y/ComposeSceneAccessibility.kt

Lines changed: 27 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -35,48 +35,46 @@ import javax.accessibility.AccessibleState
3535
import javax.accessibility.AccessibleStateSet
3636
import org.jetbrains.skiko.OS
3737
import org.jetbrains.skiko.hostOs
38+
import androidx.compose.ui.scene.ComposeSceneMediator
3839

3940
/**
40-
* This is a root [Accessible] for a [ComposeScene]
41-
*
42-
* It provides [AccessibleContext] to Swing accessibility support.
43-
* This context has no parents and provides [ComposeAccessible] as its children.
44-
*
45-
* The main purpose of this class is to support screen readers that read text under the mouse (not only, but mostly).
46-
* To support it [_accessibleContext] provides custom [ComposeSceneAccessibleContext.getAccessibleAt] implementation.
47-
*
48-
* Note about a11y for focus-based tools (e.g. VoiceOver).
49-
* Now focus-based tools are supported on [org.jetbrains.skiko.HardwareLayer] side.
50-
* When Compose's [androidx.compose.ui.semantics.SemanticsNode] is focused
51-
* [AccessibilityController.onFocusReceived] is called and
52-
* [org.jetbrains.skiko.HardwareLayer] provides mapped [ComposeAccessible] to accessibility tool.
41+
* Manages the accessibility aspects of the Compose scene for [ComposeSceneMediator].
5342
*
5443
* @see AccessibilityController
5544
* @see ComposeAccessible
5645
*/
57-
internal class ComposeSceneAccessible(
46+
internal class ComposeSceneAccessibility(
5847
private val isWindowLevel: Boolean = false,
59-
private val forceEnableA11y: Boolean = false,
6048
private val sceneRoot: () -> Component,
6149
private val accessibilityControllersProvider: () -> List<AccessibilityController>,
62-
) : Accessible {
63-
private val a11yEnabled by lazy {
64-
forceEnableA11y || (
65-
(System.getProperty("compose.accessibility.enable") != "false") &&
66-
(System.getenv("COMPOSE_DISABLE_ACCESSIBILITY") == null)
67-
)
50+
) {
51+
val enabled by lazy {
52+
(System.getProperty("compose.accessibility.enable") != "false") &&
53+
(System.getenv("COMPOSE_DISABLE_ACCESSIBILITY") == null)
6854
}
6955

70-
private val _accessibleContext by lazy {
56+
// Exposed for the benefit of tests
57+
val sceneAccessibleContext by lazy {
7158
ComposeSceneAccessibleContext()
7259
}
7360

74-
// Declare ComposeSceneAccessibleContext as the return type for the benefit of tests
75-
override fun getAccessibleContext(): ComposeSceneAccessibleContext? {
76-
if (!a11yEnabled) {
77-
return null
78-
}
79-
return _accessibleContext
61+
private val accessibleFocusHelper by lazy {
62+
AccessibleFocusHelper(sceneRoot(), sceneAccessibleContext)
63+
}
64+
65+
val accessibleContextProvider: ((Component) -> AccessibleContext)?
66+
get() = if (enabled) { _ -> accessibleFocusHelper.accessibleContext } else null
67+
68+
fun requestFocusOnAccessible(accessible: Accessible) {
69+
accessibleFocusHelper.requestFocusOnAccessible(accessible)
70+
}
71+
72+
fun accessibleParentOverride(accessible: Accessible): Accessible? {
73+
return accessibleFocusHelper.accessibleParentOverride(accessible)
74+
}
75+
76+
fun accessible(): Accessible? {
77+
return sceneRoot() as? Accessible
8078
}
8179

8280
fun indexOfChild(controller: AccessibilityController): Int {
@@ -151,7 +149,7 @@ internal class ComposeSceneAccessible(
151149
}
152150
}
153151

154-
return this@ComposeSceneAccessible
152+
return sceneRoot() as Accessible
155153
}
156154

157155
override fun contains(p: Point): Boolean = true

compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/scene/ComposeContainer.desktop.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,6 @@ internal class ComposeContainer(
168168

169169
val contentComponent by mediator::contentComponent
170170
val focusManager by mediator::focusManager
171-
val accessible by mediator::accessible
172171
var rootForTestListener by mediator::rootForTestListener
173172
// TODO: Changing fullscreen probably will require recreate our layers
174173
// It will require add this flag as remember parameters in rememberComposeSceneLayer

compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/scene/ComposeSceneMediator.desktop.kt

Lines changed: 11 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,7 @@ import androidx.compose.ui.platform.PlatformWindowContext
5959
import androidx.compose.ui.platform.ViewConfiguration
6060
import androidx.compose.ui.platform.WindowInfo
6161
import androidx.compose.ui.platform.a11y.AccessibilityController
62-
import androidx.compose.ui.platform.a11y.AccessibleFocusHelper
63-
import androidx.compose.ui.platform.a11y.ComposeSceneAccessible
62+
import androidx.compose.ui.platform.a11y.ComposeSceneAccessibility
6463
import androidx.compose.ui.scene.skia.SkiaLayerComponent
6564
import androidx.compose.ui.semantics.SemanticsOwner
6665
import androidx.compose.ui.unit.Density
@@ -148,25 +147,19 @@ internal class ComposeSceneMediator(
148147
private val _platformContext = DesktopPlatformContext()
149148
val platformContext: PlatformContext get() = _platformContext
150149

150+
val accessibility = ComposeSceneAccessibility(
151+
isWindowLevel = isWindowLevel,
152+
sceneRoot = { skiaLayerComponent.contentRoot },
153+
accessibilityControllersProvider = { semanticsOwnerManager.accessibilityControllers }
154+
)
155+
151156
private val skiaLayerComponent: SkiaLayerComponent by lazy { skiaLayerComponentFactory(this) }
152157
val contentComponent by skiaLayerComponent::hierarchyRoot
153158
var fullscreen by skiaLayerComponent::fullscreen
154159
val windowHandle by skiaLayerComponent::windowHandle
155160
val renderApi by skiaLayerComponent::renderApi
156161
val semanticsOwners: Collection<SemanticsOwner> by semanticsOwnerManager::semanticsOwners
157162

158-
val accessible: ComposeSceneAccessible = ComposeSceneAccessible(
159-
isWindowLevel = isWindowLevel,
160-
sceneRoot = { skiaLayerComponent.contentRoot },
161-
accessibilityControllersProvider = { semanticsOwnerManager.accessibilityControllers }
162-
)
163-
164-
private val accessibleFocusHelper by lazy {
165-
AccessibleFocusHelper(skiaLayerComponent.contentRoot, accessible)
166-
}
167-
168-
fun getAccessibleContext() = accessibleFocusHelper.accessibleContext
169-
170163
/**
171164
* @see ComposeFeatureFlags.useInteropBlending
172165
*/
@@ -785,17 +778,17 @@ internal class ComposeSceneMediator(
785778
_accessibilityControllers[semanticsOwner] = AccessibilityController(
786779
owner = semanticsOwner,
787780
desktopComponent = platformComponent,
788-
parentAccessible = accessible,
781+
sceneAccessibility = accessibility,
789782
onFocusReceived = {
790783
// requestFocusOnAccessible fires focusGained events, which in turn
791784
// can call this method themselves, so we need to prevent infinite recursion
792785
if (requestingFocus) return@AccessibilityController
793-
requestingFocus = true
794786

795-
val target = it ?: accessible.defaultAccessibilityFocusTarget()
787+
val target = it ?: accessibility.defaultAccessibilityFocusTarget()
796788
if (target != null) {
789+
requestingFocus = true
797790
try {
798-
accessibleFocusHelper.requestFocusOnAccessible(target)
791+
accessibility.requestFocusOnAccessible(target)
799792
} finally {
800793
requestingFocus = false
801794
}

compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/scene/skia/SwingSkiaLayerComponent.desktop.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ internal class SwingSkiaLayerComponent(
4747
object : SkiaSwingLayer(
4848
renderDelegate = renderDelegate,
4949
analytics = skiaLayerAnalytics,
50-
accessibleContextProvider = { mediator.getAccessibleContext() },
50+
accessibleContextProvider = mediator.accessibility.accessibleContextProvider
5151
) {
5252
private var endCompositionWorkaround: InputMethodEndCompositionWorkaround? = null
5353

0 commit comments

Comments
 (0)