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

Support accessibility in SkiaSwingLayer (#920)

parent 57dea506
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
*/
internal external fun initializeCAccessible(accessible: Accessible)
\ No newline at end of file
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
......@@ -6,10 +6,7 @@ import org.jetbrains.skiko.redrawer.dispatcherToBlockOn
import java.awt.Canvas
import java.awt.Component
import java.awt.Graphics
import java.awt.KeyboardFocusManager
import java.awt.event.FocusEvent
import java.awt.event.InputMethodEvent
import java.beans.PropertyChangeEvent
import javax.accessibility.Accessible
import javax.accessibility.AccessibleContext
......@@ -29,7 +26,7 @@ internal open class HardwareLayer(
}
open fun dispose() {
resetFocusAccessibleJob?.cancel()
nativeAccessibleFocusHelper.dispose()
nativeDispose()
}
......@@ -61,47 +58,18 @@ internal open class HardwareLayer(
private external fun getWindowHandle(platformInfo: Long): Long
private external fun getCurrentDPI(platformInfo: Long): Int
private val _externalAccessible = externalAccessibleFactory?.invoke(this)
private var _focusedAccessible: Accessible? = null
@Suppress("LeakingThis")
private val nativeAccessibleFocusHelper = NativeAccessibleFocusHelper(
component = this,
externalAccessible = externalAccessibleFactory?.invoke(this)
)
override fun getAccessibleContext(): AccessibleContext {
val res = (_focusedAccessible ?: _externalAccessible)?.accessibleContext
return res ?: super.getAccessibleContext()
return nativeAccessibleFocusHelper.accessibleContext ?: super.getAccessibleContext()
}
private var resetFocusAccessibleJob: Job? = null
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(this, FocusEvent.FOCUS_GAINED)
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) }
nativeAccessibleFocusHelper.requestNativeFocusOnAccessible(accessible)
}
}
......@@ -144,7 +112,7 @@ internal fun layerFrameLimiter(
*
* 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 [requestNativeFocusOnAccessible], wrong mapping can be built
* 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,
......
......@@ -3,10 +3,13 @@ package org.jetbrains.skiko.swing
import org.jetbrains.skia.Canvas
import org.jetbrains.skiko.*
import org.jetbrains.skiko.redrawer.RedrawerManager
import java.awt.Component
import java.awt.Graphics2D
import java.awt.GraphicsConfiguration
import javax.accessibility.Accessible
import javax.accessibility.AccessibleContext
import javax.swing.JComponent
import javax.swing.JPanel
import javax.swing.SwingUtilities.isEventDispatchThread
/**
......@@ -24,7 +27,8 @@ import javax.swing.SwingUtilities.isEventDispatchThread
open class SkiaSwingLayer(
renderDelegate: SkikoRenderDelegate,
analytics: SkiaLayerAnalytics = SkiaLayerAnalytics.Empty,
) : JComponent() {
externalAccessibleFactory: ((Component) -> Accessible)? = null,
) : JPanel() {
internal companion object {
init {
Library.load()
......@@ -118,7 +122,17 @@ 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?) {
// TODO: support accessibility
nativeAccessibleFocusHelper.requestNativeFocusOnAccessible(accessible)
}
}
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