1 /*
<lambda>null2 * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3 */
4
5 package kotlinx.coroutines.javafx
6
7 import javafx.animation.*
8 import javafx.application.*
9 import javafx.event.*
10 import javafx.util.*
11 import kotlinx.coroutines.*
12 import kotlinx.coroutines.internal.*
13 import java.lang.UnsupportedOperationException
14 import java.lang.reflect.*
15 import java.util.concurrent.*
16 import kotlin.coroutines.*
17
18 /**
19 * Dispatches execution onto JavaFx application thread and provides native [delay] support.
20 */
21 @Suppress("unused")
22 public val Dispatchers.JavaFx: JavaFxDispatcher
23 get() = kotlinx.coroutines.javafx.JavaFx
24
25 /**
26 * Dispatcher for JavaFx application thread with support for [awaitPulse].
27 *
28 * This class provides type-safety and a point for future extensions.
29 */
30 public sealed class JavaFxDispatcher : MainCoroutineDispatcher(), Delay {
31
32 /** @suppress */
33 override fun dispatch(context: CoroutineContext, block: Runnable): Unit = Platform.runLater(block)
34
35 /** @suppress */
36 override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation<Unit>) {
37 val timeline = schedule(timeMillis) {
38 with(continuation) { resumeUndispatched(Unit) }
39 }
40 continuation.invokeOnCancellation { timeline.stop() }
41 }
42
43 /** @suppress */
44 override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle {
45 val timeline = schedule(timeMillis) {
46 block.run()
47 }
48 return DisposableHandle { timeline.stop() }
49 }
50
51 private fun schedule(timeMillis: Long, handler: EventHandler<ActionEvent>): Timeline =
52 Timeline(KeyFrame(Duration.millis(timeMillis.toDouble()), handler)).apply { play() }
53 }
54
55 internal class JavaFxDispatcherFactory : MainDispatcherFactory {
createDispatchernull56 override fun createDispatcher(allFactories: List<MainDispatcherFactory>): MainCoroutineDispatcher = JavaFx
57
58 override val loadPriority: Int
59 get() = 1 // Swing has 0
60 }
61
62 private object ImmediateJavaFxDispatcher : JavaFxDispatcher() {
63 override val immediate: MainCoroutineDispatcher
64 get() = this
65
66 override fun isDispatchNeeded(context: CoroutineContext): Boolean = !Platform.isFxApplicationThread()
67
68 override fun toString() = toStringInternalImpl() ?: "JavaFx.immediate"
69 }
70
71 /**
72 * Dispatches execution onto JavaFx application thread and provides native [delay] support.
73 */
74 internal object JavaFx : JavaFxDispatcher() {
75 init {
76 // :kludge: to make sure Toolkit is initialized if we use JavaFx dispatcher outside of JavaFx app
77 initPlatform()
78 }
79
80 override val immediate: MainCoroutineDispatcher
81 get() = ImmediateJavaFxDispatcher
82
toStringnull83 override fun toString() = toStringInternalImpl() ?: "JavaFx"
84 }
85
86 private val pulseTimer by lazy {
87 PulseTimer().apply { start() }
88 }
89
90 /**
91 * Suspends coroutine until next JavaFx pulse and returns time of the pulse on resumption.
92 * If the [Job] of the current coroutine is completed while this suspending function is waiting, this function
93 * immediately resumes with [CancellationException][kotlinx.coroutines.CancellationException].
94 */
awaitPulsenull95 public suspend fun awaitPulse(): Long = suspendCancellableCoroutine { cont ->
96 pulseTimer.onNext(cont)
97 }
98
99 private class PulseTimer : AnimationTimer() {
100 private val next = CopyOnWriteArrayList<CancellableContinuation<Long>>()
101
handlenull102 override fun handle(now: Long) {
103 val cur = next.toTypedArray()
104 next.clear()
105 for (cont in cur)
106 with (cont) { JavaFx.resumeUndispatched(now) }
107 }
108
onNextnull109 fun onNext(cont: CancellableContinuation<Long>) {
110 next += cont
111 }
112 }
113
114 /** @return true if initialized successfully, and false if no display is detected */
initPlatformnull115 internal fun initPlatform(): Boolean = PlatformInitializer.success
116
117 // Lazily try to initialize JavaFx platform just once
118 private object PlatformInitializer {
119 @JvmField
120 val success = run {
121 /*
122 * Try to instantiate JavaFx platform in a way which works
123 * both on Java 8 and Java 11 and does not produce "illegal reflective access".
124 */
125 try {
126 val runnable = Runnable {}
127 // Invoke the public API if it is present.
128 runCatching {
129 Class.forName("javafx.application.Platform")
130 .getMethod("startup", java.lang.Runnable::class.java)
131 }.map { method ->
132 method.invoke(null, runnable)
133 return@run true
134 }
135 // If we are here, it means the public API is not present. Try the private API.
136 Class.forName("com.sun.javafx.application.PlatformImpl")
137 .getMethod("startup", java.lang.Runnable::class.java)
138 .invoke(null, runnable)
139 true
140 } catch (exception: InvocationTargetException) {
141 // Can only happen as a result of [Method.invoke].
142 val cause = exception.cause!!
143 when {
144 // Maybe the problem is that JavaFX is already initialized? Everything is good then.
145 cause is IllegalStateException && "Toolkit already initialized" == cause.message -> true
146 // If the problem is the headless environment, it is okay.
147 cause is UnsupportedOperationException && "Unable to open DISPLAY" == cause.message -> false
148 // Otherwise, the exception demonstrates an anomaly.
149 else -> throw cause
150 }
151 }
152 }
153 }
154