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

Erratic window focus behavior #4803

Closed
mgroth0 opened this issue May 11, 2024 · 2 comments
Closed

Erratic window focus behavior #4803

mgroth0 opened this issue May 11, 2024 · 2 comments

Comments

@mgroth0
Copy link

mgroth0 commented May 11, 2024

Describe the bug

A window will only focus properly if certain conditions are met.

While researching this issue, I came accross the WindowAdapter workaround in #4231 by @m-sasha. This is one of the requirements that must be met, but it alone is not enough.

In the scenario we have two windows. Window 1 has a text field, and the window 2 has a button. The button in window 2 should focus window 1 and its text field so that the user can start typing.

The expectation is that it will just work without complication.

What actually happens is that numerous strange requirements exist. It can work, but only if all of the following are true:

  1. We must use the WindowAdapter workaround, but the simple version in which focus.requestFocus() is called right away only works about 50% of the time. In order for it to work 100% of the time, we need to run it after a short delay.
  2. The compose content of the window that is being focused must be clicked at least once. The button will not work at all until it is clicked. Clicking the empty space in the window is enough, but clicking the decoration bar is not.
  3. Both the ComposeWindow and the FocusRequester must request focus (if there are multiple components in the window with the text field)

Requirement 3 is not a bug, but 1 and 2 are definitely bugs. The reason I include 3 here is to emphasize that even without 1 and 2, this operation is already quite complex and easy to mess up, which increases the cognitive burden caused by 1 and 2

Affected platforms

  • Desktop (M1 Mac OS). No other machine tested.

Versions

  • Libraries:
    • Compose Multiplatform version: 1.6.2
  • Kotlin version: 2.0.0-RC1
  • OS version(s) (required for Desktop and iOS issues): Mac OS 14.4.1
  • OS architecture (x86 or arm64): arm64
  • JDK (for desktop issues): 20.0.2+9

To Reproduce

Part 1

  1. Run the code, do not touch the window titled "Window 1"
  2. Click the "Focus TextField" button
  3. Observe that Window 1 lights up as if it is focused, but typing does not work
  4. Mash the "Focus TextField" out of frustration (still without clicking "Window 1")
  5. Mash some keys
  6. Observe still no text in text field
  7. Carefully click the title bar of "Window 1". Drag it around a bit.
  8. Click "Focus TextField"
  9. Try to type
  10. Observe still no typed text on screen
  11. Finally, click the empty space below the text field in "Window 1" once
  12. Click "Focus TextField"
  13. Type something, and observe it works
  14. At this point, it will fully work for the remainder of the process. You can select any window from any application, do whatever, and observe the "Focus TextField" button now works 100% of the time. It is unbreakable. All it needed was for some empty space in "Window 1" to be clicked once.

Part 2

  1. Edit the code to remove this part:
scope.launch {
    delay(100)
    javax.swing.SwingUtilities.invokeLater {
        focus.requestFocus()
    }
}
  1. Repeat the steps from Part 1.
  2. Observe that the button works 50% of the time. It seems rhythmic, too. Like once it works, then once it doesn't work, then once it works, and so on.
  3. Observe that even if you click random windows in between button presses, that rhythm still exists.

Context

In the end, it seems that the second issue does have an ok workaround. It adds complexity to the code and the variable delay also adds room for error, but its ok.

But the first issue is worse, in my opinion, because the only workaround I have found so far is that the user has to literally click the window, if that could even be considered a workaround. Maybe java.awt.Robot could click the window, but I don't think I would want to go there so I didn't try it.

import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.awt.ComposeWindow
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.WindowPosition
import androidx.compose.ui.window.WindowState
import androidx.compose.ui.window.application
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.awt.event.WindowAdapter
import java.awt.event.WindowEvent

fun main() {
    val state1 = WindowState()
    val state2 = WindowState()
    // position the two windows next to eachother. 
    // They are negative for my external monitor, but this can be changed
    state1.position = WindowPosition(-1300.dp, -600.dp)
    state1.size = DpSize(300.dp, 300.dp)
    state2.position = WindowPosition(-900.dp, -600.dp)
    state2.size = DpSize(300.dp, 300.dp)
    var window1: ComposeWindow? = null
    val focus = FocusRequester()
    application {
        Window(
            state = state1,
            onCloseRequest = ::exitApplication,
            title = "Window 1"
        ) {
            window1 = window
            val s = remember { mutableStateOf("") }
            TextField(
                s.value,
                onValueChange = {
                    s.value = it
                },
                modifier = Modifier.focusRequester(focus)
            )
            val scope = rememberCoroutineScope()
            DisposableEffect(window) {
                val listener =
                    object : WindowAdapter() {
                        override fun windowActivated(e: WindowEvent) {
                            javax.swing.SwingUtilities.invokeLater {
                                focus.requestFocus()
                            }
                            scope.launch {
                                delay(100)
                                javax.swing.SwingUtilities.invokeLater {
                                    focus.requestFocus()
                                }
                            }
                        }
                    }
                window.addWindowListener(listener)
                onDispose {
                    window.removeWindowListener(listener)
                }
            }
        }
        Window(
            state = state2,
            onCloseRequest = ::exitApplication,
            title = "Window 2"
        ) {
            Button(
                onClick = {
                    window1!!.requestFocus()
                    focus.requestFocus()
                }
            ) {
                Text("Focus TextField")
            }
        }
    }
}
@mgroth0 mgroth0 added bug Something isn't working submitted labels May 11, 2024
@mgroth0 mgroth0 changed the title Eratic window focus behavior Erratic window focus behavior May 11, 2024
@eymar
Copy link
Collaborator

eymar commented May 14, 2024

Reproduced with 1.6.10-rc01 too.

@m-sasha
Copy link
Contributor

m-sasha commented May 21, 2024

The problem here is that you're calling Window.requestFocus, which transfers focus to the window component itself. Compose, then, doesn't receive the key events, because they go to the window, rather than to the component on which Compose listens to key events.

You simply need to do window.toFront() instead.

P.S. All the workarounds in #4231 are needed because in that ticket the window receiving focus does not exist yet when the request is being made (and the app is not even in the foreground), which is not the case here.

@m-sasha m-sasha closed this as not planned Won't fix, can't repro, duplicate, stale May 21, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants