diff --git a/core/native/src/main/kotlin/win32/WindowsService.kt b/core/native/src/main/kotlin/win32/WindowsService.kt index 12c910b..4817472 100644 --- a/core/native/src/main/kotlin/win32/WindowsService.kt +++ b/core/native/src/main/kotlin/win32/WindowsService.kt @@ -8,11 +8,8 @@ import com.sun.jna.platform.win32.WinBase import com.sun.jna.platform.win32.WinDef.HWND import com.sun.jna.platform.win32.WinNT import com.sun.jna.platform.win32.WinNT.HANDLE -import com.sun.jna.platform.win32.WinNT.INFINITE import com.sun.jna.platform.win32.WinUser -import kotlinx.coroutines.suspendCancellableCoroutine import java.awt.Component -import kotlin.coroutines.resume class WindowsService { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9a92370..6ced2cb 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -34,3 +34,4 @@ jackson-dataformat = { module = "com.fasterxml.jackson.dataformat:jackson-datafo jna = "net.java.dev.jna:jna-platform:5.12.1" oshi = "com.github.oshi:oshi-core:6.6.5" +jnativehook = "com.github.kwhat:jnativehook:2.2.2" \ No newline at end of file diff --git a/target/server/build.gradle.kts b/target/server/build.gradle.kts index d1ee123..429e3c1 100644 --- a/target/server/build.gradle.kts +++ b/target/server/build.gradle.kts @@ -11,6 +11,7 @@ group = "br.com.firstsoft" version = "0.0.1" dependencies { + implementation(libs.jnativehook) implementation(libs.kotlinx.serialization) implementation(compose.desktop.currentOs) diff --git a/target/server/src/main/kotlin/br/com/firstsoft/target/server/ServerMain.kt b/target/server/src/main/kotlin/br/com/firstsoft/target/server/ServerMain.kt index 2eb9e32..f418fc4 100644 --- a/target/server/src/main/kotlin/br/com/firstsoft/target/server/ServerMain.kt +++ b/target/server/src/main/kotlin/br/com/firstsoft/target/server/ServerMain.kt @@ -19,6 +19,12 @@ import androidx.compose.ui.window.WindowPlacement 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.keyboard.NativeKeyEvent +import com.github.kwhat.jnativehook.keyboard.NativeKeyListener +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.receiveAsFlow import ui.PREFERENCE_START_MINIMIZED import ui.app.Overlay import ui.app.OverlaySettings @@ -27,7 +33,6 @@ import win32.WindowsService import java.awt.GraphicsEnvironment import java.awt.Toolkit - val positions = listOf( Alignment.TopStart, Alignment.TopCenter, @@ -37,18 +42,45 @@ val positions = listOf( Alignment.BottomEnd, ) -fun main() = application { - var overlaySettings by remember { mutableStateOf(OverlaySettings()) } +fun main() { + val channel = Channel() + + GlobalScreen.registerNativeHook() + GlobalScreen.addNativeKeyListener(object : NativeKeyListener { + override fun nativeKeyReleased(nativeEvent: NativeKeyEvent) { + val isCtrl = nativeEvent.modifiers.and(NativeKeyEvent.CTRL_MASK) > 0 + val isAlt = nativeEvent.modifiers.and(NativeKeyEvent.VC_ALT) > 0 + val isF10 = nativeEvent.keyCode == NativeKeyEvent.VC_F10 + + if (isCtrl && isAlt && isF10) { + channel.trySend(Unit) + } + } + }) + + application { + var overlaySettings by remember { mutableStateOf(OverlaySettings()) } - OverlayWindow(overlaySettings) + OverlayWindow(channel, overlaySettings) - SettingsWindow { - overlaySettings = it + SettingsWindow { + overlaySettings = it + } } } @Composable -private fun ApplicationScope.OverlayWindow(overlaySettings: OverlaySettings) { +private fun ApplicationScope.OverlayWindow( + channel: Channel, + overlaySettings: OverlaySettings +) { + var isVisible by remember { mutableStateOf(true) } + LaunchedEffect(Unit) { + channel.receiveAsFlow().collectLatest { + isVisible = !isVisible + } + } + val alignment = remember(overlaySettings.positionIndex) { positions[overlaySettings.positionIndex] } val overlayState = rememberWindowState().apply { size = if (overlaySettings.isHorizontal) DpSize(1024.dp, 80.dp) else DpSize(350.dp, 1024.dp) @@ -64,14 +96,14 @@ private fun ApplicationScope.OverlayWindow(overlaySettings: OverlaySettings) { Window( state = overlayState, onCloseRequest = { exitApplication() }, - visible = true, + visible = isVisible, title = "Clean Meter", resizable = false, alwaysOnTop = true, transparent = true, undecorated = true, focusable = false, - enabled = false + enabled = false, ) { LaunchedEffect(alignment, overlaySettings.selectedDisplayIndex) { val location = when (alignment) { @@ -113,11 +145,9 @@ private fun ApplicationScope.OverlayWindow(overlaySettings: OverlaySettings) { } @Composable -private fun ApplicationScope.SettingsWindow(onOverlaySettings: (OverlaySettings) -> Unit) { - val icon = painterResource("imgs/logo.png") - val state = rememberWindowState().apply { - size = DpSize(500.dp, 500.dp) - } +private fun ApplicationScope.SettingsWindow( + onOverlaySettings: (OverlaySettings) -> Unit +) { var isVisible by remember { mutableStateOf( PreferencesRepository.getPreferenceBooleanNullable( @@ -125,6 +155,10 @@ private fun ApplicationScope.SettingsWindow(onOverlaySettings: (OverlaySettings) )?.not() ?: true ) } + val icon = painterResource("imgs/logo.png") + val state = rememberWindowState().apply { + size = DpSize(500.dp, 500.dp) + } Window( state = state, @@ -142,7 +176,13 @@ private fun ApplicationScope.SettingsWindow(onOverlaySettings: (OverlaySettings) icon = icon, onAction = { isVisible = true }, menu = { - Item("Quit", onClick = ::exitApplication) + Item("Quit", onClick = { + try { + GlobalScreen.unregisterNativeHook() + } catch (e: Exception) { + } + exitApplication() + }) } ) }