Unverified Commit d6e53c50 authored by Alexander Maryanovsky's avatar Alexander Maryanovsky Committed by GitHub

Remove `NativeAccessibleFocusHelper` (#1147)

parent 3799325e
package org.jetbrains.skiko
import kotlinx.coroutines.*
import java.awt.Component
import java.awt.KeyboardFocusManager
import java.awt.event.FocusEvent
import java.beans.PropertyChangeEvent
import javax.accessibility.Accessible
import javax.accessibility.AccessibleContext
/**
* See [nativeInitializeAccessible] doc for details
* Calls `sun.lwawt.macosx.CAccessible.getCAccessible(Accessible)` on the given [accessible] object; does nothing
* if running on a non-Mac platform or in a JVM where there is no `CAccessible` class.
*
* Ideally, this and nativeInitializeAccessible should be in Compose, not Skiko.
* Unfortunately, Compose doesn't currently allow native code, and implementing it via reflection is not
* possible due to java.desktop module access restrictions:
* > class androidx.compose.ui.platform.a11y.AccessibilityKt cannot access class sun.lwawt.macosx.CAccessible
* > (in module java.desktop) because module java.desktop does not export sun.lwawt.macosx to unnamed module
*
* As such, this function is not to be considered public Skiko API.
*/
internal external fun initializeCAccessible(accessible: Accessible)
/**
* A helper class for implementing requesting accessibility focus on a given accessible.
*/
internal class NativeAccessibleFocusHelper(
private val component: Component,
private val externalAccessible: Accessible?,
) {
private var focusedAccessible: Accessible? = null
val accessibleContext: AccessibleContext?
get() = (focusedAccessible ?: externalAccessible)?.accessibleContext
private var resetFocusAccessibleJob: Job? = null
@OptIn(DelicateCoroutinesApi::class)
fun requestNativeFocusOnAccessible(accessible: Accessible?) {
focusedAccessible = accessible
when (hostOs) {
OS.Windows -> requestAccessBridgeFocusOnAccessible()
OS.MacOS -> requestMacOSFocusOnAccessible(accessible)
else -> {
focusedAccessible = null
return
}
}
// Listener spawns asynchronous notification post procedure, reading current focus owner
// and its accessibility context. This timeout is used to deal with concurrency
// TODO Find more reliable procedure
resetFocusAccessibleJob?.cancel()
resetFocusAccessibleJob = GlobalScope.launch(MainUIDispatcher) {
delay(100)
focusedAccessible = null
}
}
private fun requestAccessBridgeFocusOnAccessible() {
val focusEvent = FocusEvent(component, FocusEvent.FOCUS_GAINED)
component.focusListeners.forEach { it.focusGained(focusEvent) }
}
private fun requestMacOSFocusOnAccessible(accessible: Accessible?) {
val focusManager = KeyboardFocusManager.getCurrentKeyboardFocusManager()
val listeners = focusManager.getPropertyChangeListeners("focusOwner")
val event = PropertyChangeEvent(focusManager, "focusOwner", null, accessible)
listeners.forEach { it.propertyChange(event) }
}
fun dispose() {
resetFocusAccessibleJob?.cancel()
}
}
\ No newline at end of file
external fun initializeCAccessible(accessible: Accessible)
......@@ -7,11 +7,10 @@ import java.awt.Canvas
import java.awt.Component
import java.awt.Graphics
import java.awt.event.InputMethodEvent
import javax.accessibility.Accessible
import javax.accessibility.AccessibleContext
internal open class HardwareLayer(
externalAccessibleFactory: ((Component) -> Accessible)? = null
private val accessibleContextProvider: ((Component) -> AccessibleContext)? = null
) : Canvas() {
companion object {
init {
......@@ -26,7 +25,6 @@ internal open class HardwareLayer(
}
open fun dispose() {
nativeAccessibleFocusHelper.dispose()
nativeDispose()
}
......@@ -58,18 +56,8 @@ internal open class HardwareLayer(
private external fun getWindowHandle(platformInfo: Long): Long
private external fun getCurrentDPI(platformInfo: Long): Int
@Suppress("LeakingThis")
private val nativeAccessibleFocusHelper = NativeAccessibleFocusHelper(
component = this,
externalAccessible = externalAccessibleFactory?.invoke(this)
)
override fun getAccessibleContext(): AccessibleContext {
return nativeAccessibleFocusHelper.accessibleContext ?: super.getAccessibleContext()
}
fun requestNativeFocusOnAccessible(accessible: Accessible?) {
nativeAccessibleFocusHelper.requestNativeFocusOnAccessible(accessible)
return accessibleContextProvider?.invoke(this) ?: super.getAccessibleContext()
}
}
......@@ -106,29 +94,3 @@ internal fun layerFrameLimiter(
}
)
}
/**
* This method should be called on custom [Accessible] creation (or its context if context is created lazily).
*
* JDK's accessibility support (at least for MacOS) builds mapping AccessibleContext -> Accessible.
* Some [Accessible] are built only when focus is settled and
* since we have a hack [NativeAccessibleFocusHelper.requestNativeFocusOnAccessible], wrong mapping can be built
* (ComponentAccessibleContext -> SkiaLayer instead of ComponentAccessibleContext -> ComponentAccessible).
*
* This method forces JDK's accessibility support to cache mapping ComponentAccessibleContext -> ComponentAccessible,
* if it is called on ComponentAccessibleContext creation.
*
* Related to the [issue](https://youtrack.jetbrains.com/issue/COMPOSE-176).
*/
@Suppress("unused")
fun nativeInitializeAccessible(accessible: Accessible) {
when (hostOs) {
OS.MacOS -> {
initializeCAccessible(accessible)
}
else -> {
// TODO: do we need something for Windows?
}
}
}
\ No newline at end of file
......@@ -27,7 +27,7 @@ import javax.swing.event.AncestorListener
import kotlin.math.floor
actual open class SkiaLayer internal constructor(
externalAccessibleFactory: ((Component) -> Accessible)? = null,
accessibleContextProvider: ((Component) -> AccessibleContext)? = null,
val properties: SkiaLayerProperties,
private val renderFactory: RenderFactory = RenderFactory.Default,
private val analytics: SkiaLayerAnalytics = SkiaLayerAnalytics.Empty,
......@@ -48,7 +48,7 @@ actual open class SkiaLayer internal constructor(
internal val backedLayer: HardwareLayer
constructor(
externalAccessibleFactory: ((Component) -> Accessible)? = null,
accessibleContextProvider: ((Component) -> AccessibleContext)? = null,
isVsyncEnabled: Boolean = SkikoProperties.vsyncEnabled,
isVsyncFramelimitFallbackEnabled: Boolean = SkikoProperties.vsyncFramelimitFallbackEnabled,
frameBuffering: FrameBuffering = SkikoProperties.frameBuffering,
......@@ -56,7 +56,7 @@ actual open class SkiaLayer internal constructor(
analytics: SkiaLayerAnalytics = SkiaLayerAnalytics.Empty,
pixelGeometry: PixelGeometry = PixelGeometry.UNKNOWN,
) : this(
externalAccessibleFactory,
accessibleContextProvider,
SkiaLayerProperties(
isVsyncEnabled,
isVsyncFramelimitFallbackEnabled,
......@@ -69,12 +69,12 @@ actual open class SkiaLayer internal constructor(
)
constructor(
externalAccessibleFactory: ((Component) -> Accessible)? = null,
accessibleContextProvider: ((Component) -> AccessibleContext)? = null,
properties: SkiaLayerProperties,
analytics: SkiaLayerAnalytics = SkiaLayerAnalytics.Empty,
pixelGeometry: PixelGeometry = PixelGeometry.UNKNOWN,
) : this(
externalAccessibleFactory,
accessibleContextProvider,
properties,
RenderFactory.Default,
analytics,
......@@ -89,7 +89,7 @@ actual open class SkiaLayer internal constructor(
init {
layout = null
backedLayer = object : HardwareLayer(externalAccessibleFactory) {
backedLayer = object : HardwareLayer(accessibleContextProvider) {
override fun paint(g: Graphics) {
Logger.debug { "Paint called on HardwareLayer $this" }
checkContentScale()
......@@ -683,10 +683,6 @@ actual open class SkiaLayer internal constructor(
}
}
fun requestNativeFocusOnAccessible(accessible: Accessible?) {
backedLayer.requestNativeFocusOnAccessible(accessible)
}
override fun getAccessibleContext(): AccessibleContext {
if (accessibleContext == null) {
accessibleContext = AccessibleSkiaLayer()
......
......@@ -7,7 +7,6 @@ import java.awt.Component
import java.awt.Graphics
import java.awt.Graphics2D
import java.awt.GraphicsConfiguration
import javax.accessibility.Accessible
import javax.accessibility.AccessibleContext
import javax.swing.JPanel
import javax.swing.SwingUtilities.isEventDispatchThread
......@@ -27,7 +26,7 @@ import javax.swing.SwingUtilities.isEventDispatchThread
open class SkiaSwingLayer(
renderDelegate: SkikoRenderDelegate,
analytics: SkiaLayerAnalytics = SkiaLayerAnalytics.Empty,
externalAccessibleFactory: ((Component) -> Accessible)? = null,
private val accessibleContextProvider: ((Component) -> AccessibleContext)? = null,
private val properties: SkiaLayerProperties = SkiaLayerProperties()
) : JPanel() {
internal companion object {
......@@ -126,17 +125,7 @@ open class SkiaSwingLayer(
}
}
@Suppress("LeakingThis")
private val nativeAccessibleFocusHelper = NativeAccessibleFocusHelper(
component = this,
externalAccessible = externalAccessibleFactory?.invoke(this)
)
override fun getAccessibleContext(): AccessibleContext? {
return nativeAccessibleFocusHelper.accessibleContext ?: super.getAccessibleContext()
}
fun requestNativeFocusOnAccessible(accessible: Accessible?) {
nativeAccessibleFocusHelper.requestNativeFocusOnAccessible(accessible)
return accessibleContextProvider?.invoke(this) ?: super.getAccessibleContext()
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment