Closed gtf35 closed 3 years ago
Hello! It looks like the initialization of the JFXPanel happens before the initialization of the SwingPanel, because both Swing and JavaFX have their own thread to dispatch events. So, if you try the following approach, it should solve your problem:
import androidx.compose.desktop.LocalAppWindow
import androidx.compose.desktop.Window
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.size
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.round
import java.awt.Container
import java.awt.BorderLayout
import javafx.embed.swing.JFXPanel
import javafx.application.Platform
import javafx.scene.Group
import javafx.scene.Scene
import javax.swing.JPanel
import javafx.scene.paint.Color as JFXColor
import javafx.scene.text.Font as JFXFont
import javafx.scene.text.Text as JFXText
fun main() = Window(
title = "MyApp",
size = IntSize(600, 550)
) {
// JavaFX components
val jfxpanel = remember { JFXPanel() }
val jfxtext = remember { JFXText() }
// The current container (depending on how you are using the CFD,
// this could be ComposeWindow or ComposePanel)
val container = LocalAppWindow.current.window // ComposeWindow
val counter = remember { mutableStateOf(0) }
val inc: () -> Unit = {
counter.value++
// update JavaFX text component
Platform.runLater {
jfxtext.text = "Welcome JavaFX! ${counter.value}"
}
}
Box(
modifier = Modifier.fillMaxWidth().height(60.dp).padding(top = 20.dp),
contentAlignment = Alignment.Center
) {
Text("Counter: ${counter.value}")
}
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Column(
modifier = Modifier.padding(top = 80.dp, bottom = 20.dp)
) {
Button("1. Compose Button: increment", inc)
Spacer(modifier = Modifier.height(20.dp))
// The "Box" is strictly necessary to properly sizing and positioning the JFXPanel container.
Box(
modifier = Modifier.height(200.dp).fillMaxWidth()
) {
JavaFXPanel(
root = container,
panel = jfxpanel,
// function to initialize JFXPanel, Group, Scene
onCreate = {
Platform.runLater {
val root = Group()
val scene = Scene(root, JFXColor.GRAY)
jfxtext.x = 40.0
jfxtext.y = 40.0
jfxtext.font = JFXFont(25.0)
jfxtext.text = "Welcome JavaFX! ${counter.value}"
root.children.add(jfxtext)
jfxpanel.scene = scene
}
}
)
}
Spacer(modifier = Modifier.height(20.dp))
}
}
}
@Composable
fun Button(text: String = "", action: (() -> Unit)? = null) {
Button(
modifier = Modifier.size(270.dp, 40.dp),
onClick = { action?.invoke() }
) {
Text(text)
}
}
@Composable
public fun JavaFXPanel(
root: Container,
panel: JFXPanel,
onCreate: () -> Unit
) {
val container = remember { JPanel() }
val density = LocalDensity.current.density
Layout(
content = {},
modifier = Modifier.onGloballyPositioned { childCoordinates ->
val coordinates = childCoordinates.parentCoordinates!!
val location = coordinates.localToWindow(Offset.Zero).round()
val size = coordinates.size
container.setBounds(
(location.x / density).toInt(),
(location.y / density).toInt(),
(size.width / density).toInt(),
(size.height / density).toInt()
)
container.validate()
container.repaint()
},
measurePolicy = { _, _ ->
layout(0, 0) {}
}
)
DisposableEffect(Unit) {
container.apply {
setLayout(BorderLayout(0, 0))
add(panel)
}
root.add(container)
onCreate.invoke()
onDispose {
root.remove(container)
}
}
}
Looks so coooool! Running well! Thank you very much for providing the code that is easy to reuse 💖 Thanks a lot!
Can JavaFXPanel
be used in each item in the Compose list?
That will create a lot of JFXPanel().
Is it okay to do this?
I think this is ok, but also I think that if you need to create a list from a large number of JavaFX items, you can use one JavaFXPanel as a container for a JavaFX ListView (or any other suitable component, because I am not very good at JavaFX framework) and fill it with JavaFX components with the proper styling to match your current CFD style.
I see ~, thanks a lot 💕
how do i solve "LocalAppWindow.current.window" in compose 1.0.0-beta5?
@Rsedaikin
Thank you .
@Rsedaikin
Great work with this example - currently using it with a WebView
and Mapbox.js
- works very well and I'm pleased with the performance.
I have noticed an implicit problem with using the JavaFXPanel
.
Scenerio :
1) Display a JavaFXPanel
composable using above code.
2) Remove the composable (feasible if replacing window content)
3) DisposableEffect::onDispose
called as expected, JPanel
which hosts the JFXPanel
removed from the ComponentWindow
4) Display the JavaFXPanel
again back in the same application session.
Expected : JFXPanel
displayed as before
Result : JFXPanel
is not displayed, only the JFrame
Digging deeper :
It appears the first time you add a JavaFXPanel
it will attempt to register a PlatformImpl.FinishListener
method call : JFXPanel::registerFinishListener
. This is then proxied to PlatformImpl::addListener(finishListener)
.
So far all good, however when this is called root.remove(container)
from the onDispose
lambda invocation the JPanel
will also notify the JFXPanel
that it has been removed from the window. This then invokes JFXPanel::deregisterFinishListener
which proxies a call to : PlatformImpl.removeListener(finishListener)
.
Once this method is called it will remove the listener, which makes sense for the lifecycle of this JFXPanel
. However in removing the listener this method is called PlatformImpl::checkIdle
. If the PlatformImpl.FinishListener
count hits 0 PlatformImpl::tkExit
is called and this exits all event loops.
The end result is the compose application is still running but any calls to Platform::runLater
do nothing for the rest of the application session.
Workaround :
Register a PlatformImpl.FinishListener
manually for the lifetime of the application to prevent the event loop from exiting when finished listeners are unregistered keeping the count of registered listeners at least 1 example :
fun main() = application(exitProcessOnExit = true) {
val finishListener = object : PlatformImpl.FinishListener {
override fun idle(implicitExit: Boolean) {}
override fun exitCalled() {}
}
PlatformImpl.addListener(finishListener)
Window(
title = "My App",
icon = icon,
resizable = false,
state = WindowState(
placement = Floating,
size = size),
onCloseRequest = {
PlatformImpl.removeListener(finishListener)
exitApplication()
},
content = { ..... })
}
It's work when i run the project via IDEA but after build distributive seems like webview doesn't work at all =( Maybe you know how to fix it?
It's work when i run the project via IDEA but after build distributive seems like webview doesn't work at all =( Maybe you know how to fix it?
Sorry, I've found that I'll have a lot less trouble using swing , so I've given up on JavaFX
It's work when i run the project via IDEA but after build distributive seems like webview doesn't work at all =( Maybe you know how to fix it?
Sorry, I've found that I'll have a lot less trouble using swing , so I've given up on JavaFX
but anyway it will use WEbView from JavaFX, or you know some other webview implementations without javafx ? (i wonna create chat with youtube player or site preview inside, but javaXF webview only one solution what i found... =( )
It's work when i run the project via IDEA but after build distributive seems like webview doesn't work at all =( Maybe you know how to fix it?
Same behavior happens to me. @theone55 Did you find solutions for this?
Hi @aro311 @theone55
It seems that the problem was some ClassNotFoundException
. However, Compose simply swallows any exception, which is why your app won't crash and it would simply appear that the WebView
didn't load.
To fix that, I followed the tutorial at, Configuring included JDK modules | Native distributions & local execution | JetBrains/compose-jb, and as suggested by that tutorial, I ran the suggestModules
gradle task on my project. It suggested that I add modules("java.instrument", "java.net.http", "jdk.jfr", "jdk.jsobject", "jdk.unsupported", "jdk.unsupported.desktop", "jdk.xml.dom")
under compose.desktop.application.nativeDistributions
.
Demo
Using the provided desktop-template, I made the following modifications to ./build.gradle.kts
and ./src/main/kotlin/main.kt
, and then I added a new file ./src/main/kotlin/JFXWebView.kt
(sources below):
For ./build.gradle.kts
:
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
plugins {
kotlin("jvm")
id("org.jetbrains.compose")
id("org.openjfx.javafxplugin") version "0.0.13"
}
repositories {
mavenCentral()
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
google()
}
javafx {
version = "19"
modules("javafx.swing", "javafx.web")
}
dependencies {
// Note, if you develop a library, you should use compose.desktop.common.
// compose.desktop.currentOs should be used in launcher-sourceSet
// (in a separate module for demo project and in testMain).
// With compose.desktop.common you will also lose @Preview functionality
implementation(compose.desktop.currentOs)
}
compose.desktop {
application {
mainClass = "MainKt"
nativeDistributions {
modules("java.instrument", "java.net.http", "jdk.jfr", "jdk.jsobject", "jdk.unsupported", "jdk.unsupported.desktop", "jdk.xml.dom")
targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
packageName = "KotlinJvmComposeDesktopApplication"
packageVersion = "1.0.0"
}
}
}
For ./src/main/kotlin/main.kt
:
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.ui.Modifier
import androidx.compose.ui.awt.SwingPanel
import androidx.compose.ui.window.singleWindowApplication
fun main() = singleWindowApplication {
SwingPanel(
factory = { JFXWebView() },
modifier = Modifier.fillMaxSize(),
)
}
For ./src/main/kotlin/JFXWebView.kt
:
import javafx.application.Platform
import javafx.embed.swing.JFXPanel
import javafx.scene.Scene
import javafx.scene.web.WebView
/**
* From, https://stackoverflow.com/a/26028556
*/
class JFXWebView : JFXPanel() {
init {
Platform.runLater(::initialiseJavaFXScene)
}
private fun initialiseJavaFXScene() {
val webView = WebView()
val webEngine = webView.engine
webEngine.load("https://html5test.com/")
val scene = Scene(webView)
setScene(scene)
}
}
Then run ./gradlew packageDistributionForCurrentOS
, install the resulting native distribution, and upon running, you should get something like:
P.S. Not sure if we should also worry about firewall settings, which may prevent the JavaFX WebView
from also working, but then other parts of your app that connects to the internet might also stop working.
Edit: The simplified JFXWebView
class is not my original idea. Credits goes to Luke Quinane.
Any idea Apple Sign In shows "Failed to verify your identity. Tray again." with JFX Webview?
This is how i am using webviews, but its not getting disposed properly it opens for the first time but the second time it shows blank screen.
@Composable
fun DesktopWebView(
modifier: Modifier,
url: String,
) {
val jPanel: JPanel = remember { JPanel() }
val jfxPanel = JFXPanel()
SwingPanel(
factory = {
jfxPanel.apply { buildWebView(url) }
jPanel.add(jfxPanel)
},
modifier = modifier,
)
DisposableEffect(url) { onDispose { jPanel.remove(jfxPanel) } }
}
private fun JFXPanel.buildWebView(url: String) {
Platform.runLater {
val webView = WebView()
val webEngine = webView.engine
// Set the user agent to simulate a browser for YouTube
webEngine.userAgent =
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3"
// Enable JavaScript support for YouTube embed player
webEngine.isJavaScriptEnabled = true
// Load the YouTube video using the embed URL
webEngine.load(url)
val scene = Scene(webView)
setScene(scene)
}
}
Thanks, the solve my probleam @Rsedaikin
And if someone take version hight, maybe find
val container = LocalAppWindow.current.window
compile fail
You can replace to
val container = this.window
Then is run
@ximplia-paolo-pasianot
Please check the following ticket on YouTrack for follow-ups to this issue. GitHub issues will be closed in the coming weeks.
I want to use JavaFX in Compose-jb, but Compose-jb only support swing, so I use
JFXPanel
But,,,,,
kotlin.UninitializedPropertyAccessException: lateinit property layout has not been initialized
when I useJFXPanel
My Code
Error log