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 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.Accessible
import javax.accessibility.AccessibleContext
/** /**
* See [nativeInitializeAccessible] doc for details * See [nativeInitializeAccessible] doc for details
*/ */
internal external fun initializeCAccessible(accessible: Accessible) 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 ...@@ -6,10 +6,7 @@ import org.jetbrains.skiko.redrawer.dispatcherToBlockOn
import java.awt.Canvas import java.awt.Canvas
import java.awt.Component import java.awt.Component
import java.awt.Graphics import java.awt.Graphics
import java.awt.KeyboardFocusManager
import java.awt.event.FocusEvent
import java.awt.event.InputMethodEvent import java.awt.event.InputMethodEvent
import java.beans.PropertyChangeEvent
import javax.accessibility.Accessible import javax.accessibility.Accessible
import javax.accessibility.AccessibleContext import javax.accessibility.AccessibleContext
...@@ -29,7 +26,7 @@ internal open class HardwareLayer( ...@@ -29,7 +26,7 @@ internal open class HardwareLayer(
} }
open fun dispose() { open fun dispose() {
resetFocusAccessibleJob?.cancel() nativeAccessibleFocusHelper.dispose()
nativeDispose() nativeDispose()
} }
...@@ -61,47 +58,18 @@ internal open class HardwareLayer( ...@@ -61,47 +58,18 @@ internal open class HardwareLayer(
private external fun getWindowHandle(platformInfo: Long): Long private external fun getWindowHandle(platformInfo: Long): Long
private external fun getCurrentDPI(platformInfo: Long): Int private external fun getCurrentDPI(platformInfo: Long): Int
private val _externalAccessible = externalAccessibleFactory?.invoke(this) @Suppress("LeakingThis")
private var _focusedAccessible: Accessible? = null private val nativeAccessibleFocusHelper = NativeAccessibleFocusHelper(
component = this,
externalAccessible = externalAccessibleFactory?.invoke(this)
)
override fun getAccessibleContext(): AccessibleContext { override fun getAccessibleContext(): AccessibleContext {
val res = (_focusedAccessible ?: _externalAccessible)?.accessibleContext return nativeAccessibleFocusHelper.accessibleContext ?: super.getAccessibleContext()
return res ?: super.getAccessibleContext()
} }
private var resetFocusAccessibleJob: Job? = null
fun requestNativeFocusOnAccessible(accessible: Accessible?) { fun requestNativeFocusOnAccessible(accessible: Accessible?) {
_focusedAccessible = accessible nativeAccessibleFocusHelper.requestNativeFocusOnAccessible(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) }
} }
} }
...@@ -144,7 +112,7 @@ internal fun layerFrameLimiter( ...@@ -144,7 +112,7 @@ internal fun layerFrameLimiter(
* *
* JDK's accessibility support (at least for MacOS) builds mapping AccessibleContext -> Accessible. * JDK's accessibility support (at least for MacOS) builds mapping AccessibleContext -> Accessible.
* Some [Accessible] are built only when focus is settled and * 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). * (ComponentAccessibleContext -> SkiaLayer instead of ComponentAccessibleContext -> ComponentAccessible).
* *
* This method forces JDK's accessibility support to cache mapping ComponentAccessibleContext -> ComponentAccessible, * This method forces JDK's accessibility support to cache mapping ComponentAccessibleContext -> ComponentAccessible,
......
...@@ -3,10 +3,13 @@ package org.jetbrains.skiko.swing ...@@ -3,10 +3,13 @@ package org.jetbrains.skiko.swing
import org.jetbrains.skia.Canvas import org.jetbrains.skia.Canvas
import org.jetbrains.skiko.* import org.jetbrains.skiko.*
import org.jetbrains.skiko.redrawer.RedrawerManager import org.jetbrains.skiko.redrawer.RedrawerManager
import java.awt.Component
import java.awt.Graphics2D import java.awt.Graphics2D
import java.awt.GraphicsConfiguration import java.awt.GraphicsConfiguration
import javax.accessibility.Accessible import javax.accessibility.Accessible
import javax.accessibility.AccessibleContext
import javax.swing.JComponent import javax.swing.JComponent
import javax.swing.JPanel
import javax.swing.SwingUtilities.isEventDispatchThread import javax.swing.SwingUtilities.isEventDispatchThread
/** /**
...@@ -24,7 +27,8 @@ import javax.swing.SwingUtilities.isEventDispatchThread ...@@ -24,7 +27,8 @@ import javax.swing.SwingUtilities.isEventDispatchThread
open class SkiaSwingLayer( open class SkiaSwingLayer(
renderDelegate: SkikoRenderDelegate, renderDelegate: SkikoRenderDelegate,
analytics: SkiaLayerAnalytics = SkiaLayerAnalytics.Empty, analytics: SkiaLayerAnalytics = SkiaLayerAnalytics.Empty,
) : JComponent() { externalAccessibleFactory: ((Component) -> Accessible)? = null,
) : JPanel() {
internal companion object { internal companion object {
init { init {
Library.load() Library.load()
...@@ -118,7 +122,17 @@ open class SkiaSwingLayer( ...@@ -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?) { 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