• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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