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

window.toFront() doesn't make a window active when the app is running in a tray #4231

Closed
yevhenii-nadtochii opened this issue Feb 5, 2024 · 13 comments
Labels
desktop enhancement New feature or request

Comments

@yevhenii-nadtochii
Copy link

yevhenii-nadtochii commented Feb 5, 2024

A shown window fails to become active and gain focus. The window is created with alwaysOnTop = true, so it should gain focus automatically as shown. But direct calls to window.requestFocus() and window.toFront also have no effect.

Maybe it is because the app is a background application from the start (Tray + LSUIElement = true for MacOS). Tray menu has Show window item. And a user usually have another active application at the moment he/she clicks a Show window item from a tray menu.

Code snippet
fun main() = application {
    var isVisible by remember { mutableStateOf(true) }
    Tray(
        icon = stopwatch(),
        menu = {
            Item("Show Window", onClick = { isVisible = true })
            Item("Quit", onClick = ::exitApplication)
        }
    )
    Window(
        onCloseRequest = { isVisible = false },
        visible = isVisible,
        title = "Stopwatch App",
        icon = stopwatch(),
        alwaysOnTop = true,
    ) {
        var textValue by remember { mutableStateOf("") }
        val focus = remember { FocusRequester() }

        TextField(
            value = textValue,
            onValueChange = { textValue = it },
            modifier = Modifier.focusRequester(focus)
        )

        LaunchedEffect(isVisible) {
            if (isVisible) {
                window.toFront() // Can't gain focus, the window remains inactive.
                focus.requestFocus()  // Thus, the focus is NOT passed to the text field.
            }
        }
    }
}

Affected platforms

  • Desktop (macOS)

Versions

  • Kotlin version*: 1.9.20
  • Compose Multiplatform version*: 1.5.12
  • OS version(s)* (required for Desktop and iOS issues): Sonoma 14.2.1 (23C71)
  • OS architecture (x86 or arm64): both.
  • JDK (for desktop issues): openjdk 17.0.10

To Reproduce

window-focus-reproducer.zip

  1. Run the application from the archive, wait until the tray icon appears in the top bar.
  2. Click on an empty space on your Desktop to make sure the currently active app is Finder.
  3. Go to tray icon and show a window from the tray menu.
  4. The shown window is not active, its text field is not focused.

When the currently active app is NOT MainKt, the window never gets focused on showing up. When the currently active app is MainKt, the window gets focused half the time.

Expected behavior
Top most window is active when it is shown, especially when this is requested explicitly by window.toFront() or window.requestFocus().

Screenshot 2024-02-05 at 12 12 10 PM
@yevhenii-nadtochii yevhenii-nadtochii added bug Something isn't working submitted labels Feb 5, 2024
@mazunin-v-jb mazunin-v-jb added desktop reproduced enhancement New feature or request p:high High priority and removed submitted bug Something isn't working reproduced labels Feb 5, 2024
@mazunin-v-jb
Copy link
Contributor

Hello, @yevhenii-nadtochii, thanks for submitting the issue.
Unfortunately, for now we don't have such API for desired behavior. window.toFront() don't do that work that you expect, a real window isn't visible at that moment.
Right now, as a solution it seems like you may use window.addHierarchyListener in your case.

@yevhenii-nadtochii
Copy link
Author

I've tried this:

window.addHierarchyListener {
    window.toFront()
    window.requestFocus()
}

But the window remains inactive.

I've also tried with runDistributable (i.e., alwaysOnTop doesn't work if I run the app from IDEA). The result is the same.

@RafaelAthosPrime
Copy link

Is there any way to check if the window is on the front, a boolean function?

@m-sasha
Copy link
Member

m-sasha commented Feb 19, 2024

You can use this to cause your window to move to front and become focusable when shown:

        LaunchedEffect(Unit) {
            focus.requestFocus()
        }

        DisposableEffect(window) {
            val listener = object: ComponentAdapter() {
                override fun componentShown(e: ComponentEvent?) {
                    super.componentShown(e)
                    window.toFront()
                    window.requestFocus()
                }
            }

            window.addComponentListener(listener)

            onDispose {
                window.removeComponentListener(listener)
            }
        }

@m-sasha m-sasha removed the p:high High priority label Feb 20, 2024
@yevhenii-nadtochii
Copy link
Author

@m-sasha unfortunately this doesn't help.

@m-sasha
Copy link
Member

m-sasha commented Feb 21, 2024

Can you post a new reproducer that uses that workaround?

@yevhenii-nadtochii
Copy link
Author

@m-sasha
Copy link
Member

m-sasha commented Feb 23, 2024

Try this:

fun main() = application {
    var isVisible by remember { mutableStateOf(false) }
    Tray(
        icon = stopwatch(),
        menu = {
            Item("Show Window", onClick = { isVisible = true })
            Item("Quit", onClick = ::exitApplication)
        }
    )
    Window(
        onCloseRequest = { isVisible = false },
        visible = isVisible,
        title = "Stopwatch App",
        icon = stopwatch(),
        alwaysOnTop = true,
    ) {
        var textValue by remember { mutableStateOf("") }
        val focus = remember { FocusRequester() }

        TextField(
            value = textValue,
            onValueChange = { textValue = it },
            modifier = Modifier.focusRequester(focus)
        )

        LaunchedEffect(Unit) {
            focus.requestFocus()
        }

        DisposableEffect(window) {
            val componentListener = object: ComponentAdapter() {
                override fun componentShown(e: ComponentEvent?) {
                    window.toFront()
                }
            }
            val windowListener = object: WindowAdapter() {
                override fun windowActivated(e: WindowEvent?) {
                    SwingUtilities.invokeLater {
                        focus.requestFocus()
                    }
                }
            }

            window.addComponentListener(componentListener)
            window.addWindowListener(windowListener)

            onDispose {
                window.removeComponentListener(componentListener)
                window.removeWindowListener(windowListener)
            }
        }
    }
}

@yevhenii-nadtochii
Copy link
Author

This workaround does better.

If the currently active app is AppKt, then both the window and the field become focused. So, you can hit Show window and start typing in the field right away.

But still no changes if the currently active app is another one, which is usually the case for tray apps.

@m-sasha
Copy link
Member

m-sasha commented Feb 23, 2024

Ok, I found the magic incantation, it's Desktop.getDesktop().requestForeground():

fun main() = application {
    var isVisible by remember { mutableStateOf(false) }
    Tray(
        icon = stopwatch(),
        menu = {
            Item("Show Window", onClick = { isVisible = true })
            Item("Quit", onClick = ::exitApplication)
        }
    )
    Window(
        onCloseRequest = { isVisible = false },
        visible = isVisible,
        title = "Stopwatch App",
        icon = stopwatch(),
        alwaysOnTop = true,
    ) {
        var textValue by remember { mutableStateOf("") }
        val focus = remember { FocusRequester() }

        TextField(
            value = textValue,
            onValueChange = { textValue = it },
            modifier = Modifier.focusRequester(focus)
        )

        LaunchedEffect(Unit) {
            focus.requestFocus()
        }

        LaunchedEffect(isVisible) {
            if (isVisible) {
                Desktop.getDesktop().requestForeground(true)
            }
        }

        DisposableEffect(window) {
            val windowListener = object: WindowAdapter() {
                override fun windowActivated(e: WindowEvent?) {
                    SwingUtilities.invokeLater {
                        focus.requestFocus()
                    }
                }
            }

            window.addWindowListener(windowListener)

            onDispose {
                window.removeWindowListener(windowListener)
            }
        }
    }
}

@yevhenii-nadtochii
Copy link
Author

yevhenii-nadtochii commented Feb 23, 2024

@m-sasha The magic did the trick! Thank you 🙂

There's interesting detail. I've noticed that sometimes a window still can't get active. One out of 3–7 attempts fails. Turns out that the launched effect is not always executed upon updates of isVisible variable (why?) and requestForeground() is not called at all.

Moving it out of Window composable finally solved the problem.

The final snippet
fun main() = application {
    var isVisible by remember { mutableStateOf(false) }

    Tray(
        icon = stopwatch(),
        menu = {
            Item("Show Window", onClick = { isVisible = true })
            Item("Quit", onClick = ::exitApplication)
        }
    )

    Window(
        onCloseRequest = { isVisible = false },
        visible = isVisible,
        title = "Stopwatch App",
        icon = stopwatch(),
        alwaysOnTop = true,
    ) {
        var textValue by remember { mutableStateOf("") }
        val focus = remember { FocusRequester() }

        TextField(
            value = textValue,
            onValueChange = { textValue = it },
            modifier = Modifier.focusRequester(focus)
        )

        DisposableEffect(window) {
            val listener = object : WindowAdapter() {
                override fun windowActivated(e: WindowEvent?) {
                    SwingUtilities.invokeLater {
                        focus.requestFocus()
                    }
                }
            }
            window.addWindowListener(listener)
            onDispose {
                window.removeWindowListener(listener)
            }
        }
    }

    LaunchedEffect(isVisible) {
        if (isVisible) {
            Desktop.getDesktop().requestForeground(true)
        }
    }
}

@m-sasha
Copy link
Member

m-sasha commented Apr 18, 2024

Closing this, as it's not an issue with Compose, but with AWT.

@okushnikov
Copy link
Collaborator

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
desktop enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

5 participants