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.9k stars 1.16k forks source link

Question: Window visible true/false - without process renewal #1897

Open Jeansen opened 2 years ago

Jeansen commented 2 years ago

I would like to hide and show a window (so there is no trace in the taskbar). Therefore, I have a global hook and I set the visible state to true or false.

That works in principle, on Linux (Gnome 41, OpenJdk 17). But if there is only one window and it is set to visible = false, restoring the value to true kills the process (not the application). The UI gets completely recreated instead of simply being displayed (made visible) again. That take a lot of tome, compared to simply making the UI visible again and also destroys any remembered state! If I have another window which is visible and then do the same, I can easily toggle the visibility of the first window and no process gets recreated.

My question now is how do I achieve what works with two windows, but with only a single window?

Here's my gradle file and a sample main class. Hotkey is CTRL-SHIFT-F10

import org.jetbrains.compose.compose
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
    kotlin("jvm") version "1.6.10"
    id("org.jetbrains.compose") version "1.0.1"
}

group = "me.marcel"
version = "1.0"

repositories {
    google()
    mavenCentral()
    maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
}

dependencies {
    implementation(compose.desktop.currentOs)
    implementation("com.github.kwhat:jnativehook:2.2.1")
}

tasks.withType<KotlinCompile> {
    kotlinOptions.jvmTarget = "11"
}

compose.desktop {
    application {
        mainClass = "MainKt"
        nativeDistributions {
            targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
            packageName = "untitled"
            packageVersion = "1.0.0"
        }
    }
}
import androidx.compose.material.Text
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.WindowPosition
import androidx.compose.ui.window.application
import androidx.compose.ui.window.rememberWindowState
import com.github.kwhat.jnativehook.GlobalScreen
import com.github.kwhat.jnativehook.NativeHookException
import com.github.kwhat.jnativehook.keyboard.NativeKeyEvent
import com.github.kwhat.jnativehook.keyboard.NativeKeyListener
import java.awt.GraphicsEnvironment

class GlobalKeyListenerExample(var isVisible: MutableState<Boolean>) : NativeKeyListener {
    override fun nativeKeyReleased(e: NativeKeyEvent) {
        println("key pressed")
        if (e.modifiers and (NativeKeyEvent.SHIFT_L_MASK or NativeKeyEvent.CTRL_L_MASK) == (NativeKeyEvent.SHIFT_L_MASK or NativeKeyEvent.CTRL_L_MASK) && e.keyCode == NativeKeyEvent.VC_F10) {
            isVisible.value = !isVisible.value
        }
    }
}

fun mainApp() = application {
    var isVisible = remember { mutableStateOf(true) }

    GlobalScreen.addNativeKeyListener(GlobalKeyListenerExample(isVisible))

    Window(
        onCloseRequest = ::exitApplication,
        undecorated = false,
        visible = true, 
       // visible = false, 
        state = rememberWindowState(
            width = GraphicsEnvironment.getLocalGraphicsEnvironment().defaultScreenDevice.displayMode.width.dp,
            height = 100.dp,
            position = WindowPosition.Aligned(Alignment.BottomCenter)
        )
    ) {
        Text("one")
    }

    Window(
        onCloseRequest = ::exitApplication,
        undecorated = false,
        visible = isVisible.value,
        state = rememberWindowState(
            width = GraphicsEnvironment.getLocalGraphicsEnvironment().defaultScreenDevice.displayMode.width.dp,
            height = 100.dp,
            position = WindowPosition.Aligned(Alignment.BottomCenter)
        )
    ) {
        Text("two")
    }
}

fun main() {
    try {
        GlobalScreen.registerNativeHook()
    } catch (ex: NativeHookException) {
        System.err.println("There was a problem registering the native hook.")
        System.err.println(ex.message)
        System.exit(1)
    }

    mainApp()
}
Jeansen commented 2 years ago

So, I still do not know why enabling and disabling the visibility causes the application main thread to "reboot". Anyway, I solved my requirement by making one Window not a Window but a Dialog and set transparent to true and also it's width and height to 0.1.dp. That way, I have a transparent 'pixel' (Dialog) on the screen that nobody sees and that has now appearance in the task bar or window list. The other Window will appear or disappear whenever - e.g. by pressing a predefined key - it's visibility is set to 'true' or false'.

okushnikov commented 2 weeks ago

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