JetBrains / compose-multiplatform

Compose Multiplatform, a modern UI framework for Kotlin that makes building performant and beautiful user interfaces easy and enjoyable.
https://jetbrains.com/lp/compose-multiplatform
Apache License 2.0
15.95k stars 1.16k forks source link

IllegalArgumentException when re-show a disposed ComposeWindow #4579

Open XYZboom opened 5 months ago

XYZboom commented 5 months ago

Describe the bug When I try to re-display a window that has already been destroyed, the following exception message appears:

java.lang.IllegalArgumentException: ComposeContainer is disposed
...

Affected platforms

Versions

To Reproduce Run this. I want set isUndecorated by click the button.

application {
    Window(onCloseRequest = ::exitApplication, undecorated = true) {
        Button(onClick = {
            try {
                window.dispose()
                window.isUndecorated = !window.isUndecorated
                window.isVisible = true
            } catch (e: Exception) {
                e.printStackTrace()
            }
        }) {
            Text("change undecorated")
        }
    }
}

Expected behavior Click the button and the isUndecorated of window changed. Works in java swing:

val frame = JFrame()
frame.isUndecorated = true
frame.defaultCloseOperation = JFrame.EXIT_ON_CLOSE
val panel = JPanel()
frame.contentPane = panel
val button = JButton()
button.text = "Change Undecorated"
button.addMouseListener(object: MouseAdapter() {
    override fun mouseClicked(e: MouseEvent?) {
        frame.dispose()
        frame.isUndecorated = !frame.isUndecorated
        frame.isVisible = true
        super.mouseClicked(e)
    }
})
panel.add(button)
frame.setSize(300, 200)
frame.isVisible = true*/

Additional context stack:

java.lang.IllegalArgumentException: ComposeContainer is disposed
    at androidx.compose.ui.awt.ComposeWindowPanel.getComposeContainer(ComposeWindowPanel.desktop.kt:61)
    at androidx.compose.ui.awt.ComposeWindowPanel.setBounds(ComposeWindowPanel.desktop.kt:132)
    at java.desktop/java.awt.BorderLayout.layoutContainer(BorderLayout.java:843)
    at java.desktop/java.awt.Container.layout(Container.java:1541)
    at java.desktop/java.awt.Container.doLayout(Container.java:1530)
    at java.desktop/java.awt.Container.validateTree(Container.java:1725)
    at java.desktop/java.awt.Container.validateTree(Container.java:1734)
    at java.desktop/java.awt.Container.validateTree(Container.java:1734)
    at java.desktop/java.awt.Container.validateTree(Container.java:1734)
    at java.desktop/java.awt.Container.validate(Container.java:1660)
    at java.desktop/java.awt.Container.validateUnconditionally(Container.java:1697)
    at java.desktop/java.awt.Window.show(Window.java:1055)
    at java.desktop/java.awt.Component.show(Component.java:1728)
    at java.desktop/java.awt.Component.setVisible(Component.java:1675)
    at java.desktop/java.awt.Window.setVisible(Window.java:1036)
    at xyzboom.theinvinciblesword.ComposableSingletons$MainKt$lambda-2$1$1$1.invoke(main.kt:87)
    at xyzboom.theinvinciblesword.ComposableSingletons$MainKt$lambda-2$1$1$1.invoke(main.kt:83)
    at androidx.compose.foundation.ClickablePointerInputNode$pointerInput$3.invoke-k-4lQ0M(Clickable.kt:991)
    at androidx.compose.foundation.ClickablePointerInputNode$pointerInput$3.invoke(Clickable.kt:984)
    at androidx.compose.foundation.gestures.TapGestureDetectorKt$detectTapAndPress$2$1.invokeSuspend(TapGestureDetector.kt:258)
    at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
    at kotlinx.coroutines.DispatchedTaskKt.resume(DispatchedTask.kt:179)
    at kotlinx.coroutines.DispatchedTaskKt.dispatch(DispatchedTask.kt:168)
    at kotlinx.coroutines.CancellableContinuationImpl.dispatchResume(CancellableContinuationImpl.kt:474)
    at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl(CancellableContinuationImpl.kt:508)
    at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl$default(CancellableContinuationImpl.kt:497)
    at kotlinx.coroutines.CancellableContinuationImpl.resumeWith(CancellableContinuationImpl.kt:368)
    at androidx.compose.ui.input.pointer.SuspendingPointerInputModifierNodeImpl$PointerEventHandlerCoroutine.offerPointerEvent(SuspendingPointerInputFilter.kt:672)
    at androidx.compose.ui.input.pointer.SuspendingPointerInputModifierNodeImpl.dispatchPointerEvent(SuspendingPointerInputFilter.kt:549)
    at androidx.compose.ui.input.pointer.SuspendingPointerInputModifierNodeImpl.onPointerEvent-H0pRuoY(SuspendingPointerInputFilter.kt:571)
    at androidx.compose.foundation.AbstractClickablePointerInputNode.onPointerEvent-H0pRuoY(Clickable.kt:950)
    at androidx.compose.foundation.AbstractClickableNode.onPointerEvent-H0pRuoY(Clickable.kt:798)
    at androidx.compose.ui.input.pointer.Node.dispatchMainEventPass(HitPathTracker.kt:326)
    at androidx.compose.ui.input.pointer.Node.dispatchMainEventPass(HitPathTracker.kt:312)
    at androidx.compose.ui.input.pointer.NodeParent.dispatchMainEventPass(HitPathTracker.kt:187)
    at androidx.compose.ui.input.pointer.HitPathTracker.dispatchChanges(HitPathTracker.kt:106)
    at androidx.compose.ui.input.pointer.PointerInputEventProcessor.process-BIzXfog(PointerInputEventProcessor.kt:114)
    at androidx.compose.ui.node.RootNodeOwner.onPointerInput(RootNodeOwner.skiko.kt:208)
    at androidx.compose.ui.scene.MultiLayerComposeSceneImpl.processRelease(MultiLayerComposeScene.skiko.kt:315)
    at androidx.compose.ui.scene.MultiLayerComposeSceneImpl.processPointerInputEvent(MultiLayerComposeScene.skiko.kt:230)
    at androidx.compose.ui.scene.BaseComposeScene$inputHandler$2.invoke(BaseComposeScene.skiko.kt:61)
    at androidx.compose.ui.scene.BaseComposeScene$inputHandler$2.invoke(BaseComposeScene.skiko.kt:61)
    at androidx.compose.ui.input.pointer.SyntheticEventSender.sendInternal(SyntheticEventSender.skiko.kt:175)
    at androidx.compose.ui.input.pointer.SyntheticEventSender.send(SyntheticEventSender.skiko.kt:79)
    at androidx.compose.ui.scene.ComposeSceneInputHandler.onPointerEvent-WlEVilQ(ComposeSceneInputHandler.skiko.kt:118)
    at androidx.compose.ui.scene.ComposeSceneInputHandler.onPointerEvent-BGSDPeU(ComposeSceneInputHandler.skiko.kt:84)
    at androidx.compose.ui.scene.BaseComposeScene.sendPointerEvent-BGSDPeU(BaseComposeScene.skiko.kt:182)
    at androidx.compose.ui.scene.ComposeScene.sendPointerEvent-BGSDPeU$default(ComposeScene.skiko.kt:180)
    at androidx.compose.ui.scene.ComposeSceneMediator_desktopKt.onMouseEvent-d-4ec7I(ComposeSceneMediator.desktop.kt:662)
    at androidx.compose.ui.scene.ComposeSceneMediator_desktopKt.access$onMouseEvent-d-4ec7I(ComposeSceneMediator.desktop.kt:1)
    at androidx.compose.ui.scene.ComposeSceneMediator.onMouseEvent(ComposeSceneMediator.desktop.kt:363)
    at androidx.compose.ui.scene.ComposeSceneMediator.access$onMouseEvent(ComposeSceneMediator.desktop.kt:64)
    at androidx.compose.ui.scene.ComposeSceneMediator$mouseListener$1.mouseReleased(ComposeSceneMediator.desktop.kt:168)
    at java.desktop/java.awt.Component.processMouseEvent(Component.java:6626)
    at java.desktop/java.awt.Component.processEvent(Component.java:6391)
    at java.desktop/java.awt.Component.dispatchEventImpl(Component.java:5001)
    at java.desktop/java.awt.Component.dispatchEvent(Component.java:4833)
    at java.desktop/java.awt.EventQueue.dispatchEventImpl(EventQueue.java:773)
    at java.desktop/java.awt.EventQueue$4.run(EventQueue.java:722)
    at java.desktop/java.awt.EventQueue$4.run(EventQueue.java:716)
    at java.base/java.security.AccessController.doPrivileged(AccessController.java:399)
    at java.base/java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(ProtectionDomain.java:86)
    at java.base/java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(ProtectionDomain.java:97)
    at java.desktop/java.awt.EventQueue$5.run(EventQueue.java:746)
    at java.desktop/java.awt.EventQueue$5.run(EventQueue.java:744)
    at java.base/java.security.AccessController.doPrivileged(AccessController.java:399)
    at java.base/java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(ProtectionDomain.java:86)
    at java.desktop/java.awt.EventQueue.dispatchEvent(EventQueue.java:743)
    at java.desktop/java.awt.EventDispatchThread.pumpOneEventForFilters(EventDispatchThread.java:203)
    at java.desktop/java.awt.EventDispatchThread.pumpEventsForFilter(EventDispatchThread.java:124)
    at java.desktop/java.awt.EventDispatchThread.pumpEventsForHierarchy(EventDispatchThread.java:113)
    at java.desktop/java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:109)
    at java.desktop/java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:101)
    at java.desktop/java.awt.EventDispatchThread.run(EventDispatchThread.java:90)
m-sasha commented 5 months ago

I don't think that's a use-case we want to support.

What are you trying to achieve here? If you want to change the decorated state of the window, pass undecorated = newValue to Window.

XYZboom commented 5 months ago

I want to achieve the ability to switch window decoration state at runtime, but in Swing, I can only dispose of the window first and then re-show it. Like this:

val frame = JFrame()
frame.isUndecorated = true
frame.defaultCloseOperation = JFrame.EXIT_ON_CLOSE
val panel = JPanel()
frame.contentPane = panel
val button = JButton()
button.text = "Change Undecorated"
button.addMouseListener(object: MouseAdapter() {
    override fun mouseClicked(e: MouseEvent?) {
        frame.dispose()
        frame.isUndecorated = !frame.isUndecorated
        frame.isVisible = true
        super.mouseClicked(e)
    }
})
panel.add(button)
frame.setSize(300, 200)
frame.isVisible = true

but IllegalArgumentException when use compose

m-sasha commented 5 months ago

Reproduder:

fun main() = application {
    var undecorated by remember { mutableStateOf(false) }
    Window(
        onCloseRequest = {},
        undecorated = undecorated
    ) {
        Button(
            onClick = {
                undecorated = !undecorated
            }
        ) {
            Text("Toggle Decorated")
        }
    }
}
m-sasha commented 5 months ago

The code (in setUndecoratedSafely) acknowledges that it's not possible change the decorated flag while the window is showing. I don't know if it's possible to support this.

XYZboom commented 5 months ago

Java swing does not support to change decorate state when frame is showing. A possible solution is: stackoverflow. But as shown above, it does not work on compose because ComposeContainer was disposed and not recreate when re-show the window.

XYZboom commented 5 months ago

The document in setUndecoratedSafely says that if isUndecorated does not change, we can set isUndecorated safely. But the effect of code is: if param value != isUndecorated, change the isUndecorated. It's actually unsafe. code here @igordmn

XYZboom commented 5 months ago

The document in setUndecoratedSafely says that if isUndecorated does not change, we can set isUndecorated safely. But the effect of code is: if param value != isUndecorated, change the isUndecorated. It's actually unsafe. code here @igordmn

Maybe we can dispose the window first and set its isUndecorated. That's totally safe. Like solution metioned above

XYZboom commented 5 months ago

@elijah-semyonov I think want I need is re-show a disposed compose window, whether manually or automaticly. But neither of these two methods works, so l believe it's a bug in compose. Can you help add a bug tag, thanks!

XYZboom commented 5 months ago

Maybe need to support ComposeSceneLayer to open again after close

m-sasha commented 5 months ago

We could fix it by disposing and re-show the window when asked to change the decorated flag on it. To allow this we need to avoid disposing the ComposePanel when the window is disposed, which is probably the right thing to do anyway.

Subject: [PATCH] Dispose and re-show window when undecorated flag changes.
---
Index: compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/util/Windows.desktop.kt
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/util/Windows.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/util/Windows.desktop.kt
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/util/Windows.desktop.kt  (revision 7ca4551a1fa7aaf09e940b8bf6d75f876564ed21)
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/util/Windows.desktop.kt  (date 1712392967318)
@@ -157,7 +157,14 @@
  */
 internal fun Frame.setUndecoratedSafely(value: Boolean) {
     if (this.isUndecorated != value) {
-        this.isUndecorated = value
+        if (isDisplayable) {
+            dispose()
+            this.isUndecorated = value
+            isVisible = true
+        }
+        else {
+            this.isUndecorated = value
+        }
     }
 }

Index: compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeWindow.desktop.kt
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeWindow.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeWindow.desktop.kt
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeWindow.desktop.kt (revision 7ca4551a1fa7aaf09e940b8bf6d75f876564ed21)
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeWindow.desktop.kt (date 1712392875993)
@@ -17,6 +17,7 @@

 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.CompositionLocalContext
+import androidx.compose.runtime.DisposableEffect
 import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.input.key.KeyEvent
@@ -157,12 +158,13 @@
             undecoratedWindowResizer.Content(
                 modifier = Modifier.layoutId("UndecoratedWindowResizer")
             )
-        }
-    }

-    override fun dispose() {
-        composePanel.dispose()
-        super.dispose()
+            DisposableEffect(Unit) {
+                onDispose {
+                    composePanel.dispose()
+                }
+            }
+        }
     }

     override fun setUndecorated(value: Boolean) {

(same for DialogWindow, of course)

XYZboom commented 5 months ago

We could fix it by disposing and re-show the window when asked to change the decorated flag on it. To allow this we need to avoid disposing the ComposePanel when the window is disposed, which is probably the right thing to do anyway.

Subject: [PATCH] Dispose and re-show window when undecorated flag changes.
---
Index: compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/util/Windows.desktop.kt
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/util/Windows.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/util/Windows.desktop.kt
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/util/Windows.desktop.kt    (revision 7ca4551a1fa7aaf09e940b8bf6d75f876564ed21)
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/util/Windows.desktop.kt    (date 1712392967318)
@@ -157,7 +157,14 @@
  */
 internal fun Frame.setUndecoratedSafely(value: Boolean) {
     if (this.isUndecorated != value) {
-        this.isUndecorated = value
+        if (isDisplayable) {
+            dispose()
+            this.isUndecorated = value
+            isVisible = true
+        }
+        else {
+            this.isUndecorated = value
+        }
     }
 }

Index: compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeWindow.desktop.kt
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeWindow.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeWindow.desktop.kt
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeWindow.desktop.kt   (revision 7ca4551a1fa7aaf09e940b8bf6d75f876564ed21)
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeWindow.desktop.kt   (date 1712392875993)
@@ -17,6 +17,7 @@

 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.CompositionLocalContext
+import androidx.compose.runtime.DisposableEffect
 import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.input.key.KeyEvent
@@ -157,12 +158,13 @@
             undecoratedWindowResizer.Content(
                 modifier = Modifier.layoutId("UndecoratedWindowResizer")
             )
-        }
-    }

-    override fun dispose() {
-        composePanel.dispose()
-        super.dispose()
+            DisposableEffect(Unit) {
+                onDispose {
+                    composePanel.dispose()
+                }
+            }
+        }
     }

     override fun setUndecorated(value: Boolean) {

(same for DialogWindow, of course)

Thanks a lot, it works. But issue 1448 occurs after this change.

XYZboom commented 5 months ago

So the problem is: the dispose function in ComposeWindow should only dispose resources temporarily but not permanently

okushnikov commented 3 weeks ago

Please check the following ticket on YouTrack for follow-ups to this issue. GitHub issues will be closed in the coming weeks.