Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SwingPanel throws NPE on focus request if nothing focusable #2512

Closed
jasonsparc opened this issue Nov 28, 2022 · 0 comments · Fixed by JetBrains/compose-multiplatform-core#340
Assignees
Labels
crash p:high High priority swing interop Swing interop issue

Comments

@jasonsparc
Copy link

Kotlin: 1.7.20
Compose: 1.2.1

If there's no component focusable inside a SwingPanel and you try to request focus to it via, e.g., tabbing, it'll crash the application.

Try the following sample code,

import androidx.compose.foundation.layout.*
import androidx.compose.material.OutlinedTextField
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.awt.SwingPanel
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.singleWindowApplication
import javax.swing.JPanel

fun main() = singleWindowApplication {
    val letsSetItToFocusable = false
    Column(
        modifier = Modifier.padding(50.dp).fillMaxSize(),
        verticalArrangement = Arrangement.spacedBy(8.dp),
    ) {
        SomethingToFocusOn()
        SwingPanel(
            factory = {
                JPanel().apply {
                    if (letsSetItToFocusable) isFocusable = true
                }
            },
            modifier = Modifier.fillMaxWidth().height(40.dp),
        )
        SomethingToFocusOn()
    }
}

@Composable
fun SomethingToFocusOn() {
    val text = remember { mutableStateOf("") }
    OutlinedTextField(value = text.value, onValueChange = { text.value = it }, singleLine = true, label = {
        Text("Try tab to switch focus")
    })
}

Outcome:

Capture

Relevant stack trace:

Exception in thread "AWT-EventQueue-0" java.lang.NullPointerException: Cannot invoke "java.awt.Component.requestFocus(java.awt.event.FocusEvent$Cause)" because the return value of "java.awt.FocusTraversalPolicy.getFirstComponent(java.awt.Container)" is null
	at androidx.compose.ui.awt.FocusSwitcher$Content$1.invoke(SwingPanel.desktop.kt:191)
	at androidx.compose.ui.awt.FocusSwitcher$Content$1.invoke(SwingPanel.desktop.kt:186)
	at androidx.compose.ui.focus.FocusChangedModifierKt$onFocusChanged$2$1.invoke(FocusChangedModifier.kt:47)
	at androidx.compose.ui.focus.FocusChangedModifierKt$onFocusChanged$2$1.invoke(FocusChangedModifier.kt:44)
	at androidx.compose.ui.focus.FocusEventModifierLocal.propagateFocusEvent(FocusEventModifier.kt:137)
	at androidx.compose.ui.focus.FocusTransactionsKt.sendOnFocusEvent(FocusTransactions.kt:279)
	at androidx.compose.ui.focus.FocusModifier.setFocusState(FocusModifier.kt:79)
	...

Now, in the above sample code, set letsSetItToFocusable = true instead, and it won't crash anymore. That's because the SwingPanel now has a focusable component.

Digging into the source code of SwingPanel, I found the following lines to be problematic:

info.container.focusTraversalPolicy
    .getFirstComponent(info.container)
    .requestFocus(FocusEvent.Cause.TRAVERSAL_FORWARD)

https://github.com/JetBrains/androidx/blob/release/1.2.1/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/SwingPanel.desktop.kt#L190

info.container.focusTraversalPolicy
    .getLastComponent(info.container)
    .requestFocus(FocusEvent.Cause.TRAVERSAL_FORWARD)

https://github.com/JetBrains/androidx/blob/release/1.2.1/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/SwingPanel.desktop.kt#L203

The problem is, both getFirstComponent(…) and getLastComponent(…) of the FocusTraversalPolicy interface are allowed to return null, and yet the kotlin code is treating it that it won't. This was confirmed via a debugger, and also if you use the IDE to look for implementations of the FocusTraversalPolicy interface, you will find that it may indeed return null when no swing component is focusable.

Suggestion: Honor the nullability of those methods, and perhaps simply skip over the SwingPanel if it has no focusable components when looking for components to focus to.

@dima-avdeev-jb dima-avdeev-jb self-assigned this Nov 29, 2022
@igordmn igordmn assigned igordmn and unassigned dima-avdeev-jb Nov 29, 2022
@igordmn igordmn added crash p:high High priority swing interop Swing interop issue labels Nov 29, 2022
igordmn added a commit to JetBrains/compose-multiplatform-core that referenced this issue Dec 1, 2022
igordmn added a commit to JetBrains/compose-multiplatform-core that referenced this issue Dec 1, 2022
igordmn added a commit to JetBrains/compose-multiplatform-core that referenced this issue Dec 5, 2022
eymar pushed a commit to JetBrains/compose-multiplatform-core that referenced this issue Jan 13, 2023
MatkovIvan pushed a commit to MatkovIvan/compose-multiplatform that referenced this issue May 10, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
crash p:high High priority swing interop Swing interop issue
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants