From 7170305b1100871affeb6833d6e3dc3e21d10f68 Mon Sep 17 00:00:00 2001 From: Yiqun Zhang Date: Thu, 23 Nov 2023 21:29:35 +0800 Subject: [PATCH 1/4] :sparkles: Determines where the interface is displayed --- composeApp/build.gradle.kts | 2 +- .../kotlin/com/clipevery/model/AppUI.kt | 5 + .../ui/window/ClipeveryTray.desktop.kt | 217 ++++++++++++++++++ .../kotlin/{ => com/clipevery}/main.kt | 86 ++++++- .../com/clipevery/ui/TrayMouseClicked.kt | 45 ++++ .../kotlin/com/clipevery/utils/UIUtils.kt | 11 + 6 files changed, 354 insertions(+), 12 deletions(-) create mode 100644 composeApp/src/commonMain/kotlin/com/clipevery/model/AppUI.kt create mode 100644 composeApp/src/desktopMain/kotlin/androidx/compose/ui/window/ClipeveryTray.desktop.kt rename composeApp/src/desktopMain/kotlin/{ => com/clipevery}/main.kt (58%) create mode 100644 composeApp/src/desktopMain/kotlin/com/clipevery/ui/TrayMouseClicked.kt create mode 100644 composeApp/src/desktopMain/kotlin/com/clipevery/utils/UIUtils.kt diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index 4816ef6dc..929b7a91a 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -44,7 +44,7 @@ kotlin { compose.desktop { application { - mainClass = "MainKt" + mainClass = "com.clipevery.MainKt" nativeDistributions { targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) diff --git a/composeApp/src/commonMain/kotlin/com/clipevery/model/AppUI.kt b/composeApp/src/commonMain/kotlin/com/clipevery/model/AppUI.kt new file mode 100644 index 000000000..fe4e7f802 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/clipevery/model/AppUI.kt @@ -0,0 +1,5 @@ +package com.clipevery.model + +import androidx.compose.ui.unit.Dp + +data class AppUI(val width: Dp, val height: Dp) diff --git a/composeApp/src/desktopMain/kotlin/androidx/compose/ui/window/ClipeveryTray.desktop.kt b/composeApp/src/desktopMain/kotlin/androidx/compose/ui/window/ClipeveryTray.desktop.kt new file mode 100644 index 000000000..a024e75da --- /dev/null +++ b/composeApp/src/desktopMain/kotlin/androidx/compose/ui/window/ClipeveryTray.desktop.kt @@ -0,0 +1,217 @@ +package androidx.compose.ui.window + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.SideEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCompositionContext +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.graphics.toAwtImage +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.LayoutDirection +import com.clipevery.platform.currentPlatform +import java.awt.PopupMenu +import java.awt.SystemTray +import java.awt.TrayIcon +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.receiveAsFlow +import java.awt.ComponentOrientation +import java.awt.GraphicsConfiguration +import java.awt.GraphicsEnvironment +import java.awt.event.MouseListener +import java.util.Locale + +// In fact, this size doesn't affect anything on Windows/Linux, because they request what they +// need, and not what we provide. It only affects macOs. This size will be scaled in asAwtImage to +// support DPI=2.0 +// Unfortunately I hadn't enough time to find sources from the official docs + +private val iconSize = run { + // https://doc.qt.io/qt-5/qtwidgets-desktop-systray-example.html (search 22x22) + if (currentPlatform().isWindows()) Size(22f, 22f) + // https://doc.qt.io/qt-5/qtwidgets-desktop-systray-example.html (search 16x16) + else if (currentPlatform().isLinux()) Size(16f, 16f) + // https://medium.com/@acwrightdesign/creating-a-macos-menu-bar-application-using-swiftui-54572a5d5f87 + else if (currentPlatform().isMacos()) Size(22f, 22f) + else Size(32f, 32f) +} + +internal val GlobalDensity get() = GraphicsEnvironment.getLocalGraphicsEnvironment() + .defaultScreenDevice + .defaultConfiguration + .density + +private val GraphicsConfiguration.density: Density + get() = Density( + defaultTransform.scaleX.toFloat(), + fontScale = 1f +) + +internal val GlobalLayoutDirection get() = Locale.getDefault().layoutDirection + +internal val Locale.layoutDirection: LayoutDirection + get() = ComponentOrientation.getOrientation(this).layoutDirection + +internal val ComponentOrientation.layoutDirection: LayoutDirection + get() = when { + isLeftToRight -> LayoutDirection.Ltr + isHorizontal -> LayoutDirection.Rtl + else -> LayoutDirection.Ltr + } + + +/** + * `true` if the platform supports tray icons in the taskbar + */ +val isTraySupported: Boolean get() = SystemTray.isSupported() + +// TODO(demin): add mouse click/double-click/right click listeners (can we use PointerInputEvent?) +/** + * Adds tray icon to the platform taskbar if it is supported. + * + * If tray icon isn't supported by the platform, in the "standard" error output stream + * will be printed an error. + * + * See [isTraySupported] to know if tray icon is supported + * (for example to show/hide an option in the application settings) + * + * @param icon Icon of the tray + * @param state State to control tray and show notifications + * @param tooltip Hint/tooltip that will be shown to the user + * @param menu Context menu of the tray that will be shown to the user on the mouse click (right + * click on Windows, left click on macOs). + * If it doesn't contain any items then context menu will not be shown. + * @param onAction Action performed when user clicks on the tray icon (double click on Windows, + * right click on macOs) + */ +@Suppress("unused") +@Composable +fun ApplicationScope.Tray( + icon: Painter, + state: TrayState = rememberTrayState(), + tooltip: String? = null, + onAction: () -> Unit = {}, + mouseListener: MouseListener, + menu: @Composable MenuScope.() -> Unit = {} +) { + if (!isTraySupported) { + DisposableEffect(Unit) { + // We should notify developer, but shouldn't throw an exception. + // If we would throw an exception, some application wouldn't work on some platforms at + // all, if developer doesn't check that application crashes. + // + // We can do this because we don't return anything in Tray function, and following + // code doesn't depend on something that is created/calculated in this function. + System.err.println( + "Tray is not supported on the current platform. " + + "Use the global property `isTraySupported` to check." + ) + onDispose {} + } + return + } + + val currentOnAction by rememberUpdatedState(onAction) + + val awtIcon = remember(icon) { + // We shouldn't use LocalDensity here because Tray's density doesn't equal it. It + // equals to the density of the screen on which it shows. Currently Swing doesn't + // provide us such information, it only requests an image with the desired width/height + // (see MultiResolutionImage.getResolutionVariant). Resources like svg/xml should look okay + // because they don't use absolute '.dp' values to draw, they use values which are + // relative to their viewport. + icon.toAwtImage(GlobalDensity, GlobalLayoutDirection, iconSize) + } + + val tray = remember { + TrayIcon(awtIcon).apply { + isImageAutoSize = true + + addActionListener { + currentOnAction() + } + + addMouseListener(mouseListener) + } + } + val popupMenu = remember { PopupMenu() } + val currentMenu by rememberUpdatedState(menu) + + SideEffect { + if (tray.image != awtIcon) tray.image = awtIcon + if (tray.toolTip != tooltip) tray.toolTip = tooltip + } + + val composition = rememberCompositionContext() + val coroutineScope = rememberCoroutineScope() + + DisposableEffect(Unit) { + tray.popupMenu = popupMenu + + val menuComposition = popupMenu.setContent(composition) { + currentMenu() + } + + SystemTray.getSystemTray().add(tray) + + state.notificationFlow + .onEach(tray::displayMessage) + .launchIn(coroutineScope) + + onDispose { + menuComposition.dispose() + SystemTray.getSystemTray().remove(tray) + } + } +} + +/** + * Creates a [WindowState] that is remembered across compositions. + */ +@Composable +fun rememberTrayState() = remember { + TrayState() +} + +/** + * A state object that can be hoisted to control tray and show notifications. + * + * In most cases, this will be created via [rememberTrayState]. + */ +class TrayState { + private val notificationChannel = Channel(0) + + /** + * Flow of notifications sent by [sendNotification]. + * This flow doesn't have a buffer, so all previously sent notifications will not appear in + * this flow. + */ + val notificationFlow: Flow + get() = notificationChannel.receiveAsFlow() + + /** + * Send notification to tray. If [TrayState] is attached to [Tray], notification will be sent to + * the platform. If [TrayState] is not attached then notification will be lost. + */ + fun sendNotification(notification: Notification) { + notificationChannel.trySend(notification) + } +} + +private fun TrayIcon.displayMessage(notification: Notification) { + val messageType = when (notification.type) { + Notification.Type.None -> TrayIcon.MessageType.NONE + Notification.Type.Info -> TrayIcon.MessageType.INFO + Notification.Type.Warning -> TrayIcon.MessageType.WARNING + Notification.Type.Error -> TrayIcon.MessageType.ERROR + } + + displayMessage(notification.title, notification.message, messageType) +} diff --git a/composeApp/src/desktopMain/kotlin/main.kt b/composeApp/src/desktopMain/kotlin/com/clipevery/main.kt similarity index 58% rename from composeApp/src/desktopMain/kotlin/main.kt rename to composeApp/src/desktopMain/kotlin/com/clipevery/main.kt index 731b891ed..2bb3807d2 100644 --- a/composeApp/src/desktopMain/kotlin/main.kt +++ b/composeApp/src/desktopMain/kotlin/com/clipevery/main.kt @@ -1,20 +1,28 @@ +package com.clipevery + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Tray import androidx.compose.ui.window.Window +import androidx.compose.ui.window.WindowPlacement +import androidx.compose.ui.window.WindowPosition import androidx.compose.ui.window.application -import com.clipevery.model.AppConfig -import com.clipevery.model.AppInfo -import com.clipevery.ClipeveryApp -import com.clipevery.Dependencies +import androidx.compose.ui.window.rememberWindowState import com.clipevery.config.ConfigManager import com.clipevery.config.FileType import com.clipevery.encrypt.CreateSignalProtocolState import com.clipevery.encrypt.SignalProtocol import com.clipevery.encrypt.getSignalProtocolFactory -import com.clipevery.getAppInfoFactory import com.clipevery.log.initLogger +import com.clipevery.model.AppConfig +import com.clipevery.model.AppInfo import com.clipevery.net.ClipServer import com.clipevery.net.DesktopClipServer import com.clipevery.path.PathProvider @@ -23,16 +31,24 @@ import com.clipevery.platform.currentPlatform import com.clipevery.presist.DesktopOneFilePersist import com.clipevery.presist.FilePersist import com.clipevery.presist.OneFilePersist +import com.clipevery.ui.getTrayMouseAdapter import com.clipevery.utils.DesktopQRCodeGenerator import com.clipevery.utils.QRCodeGenerator import com.clipevery.utils.ioDispatcher import io.github.oshai.kotlinlogging.KotlinLogging import kotlinx.coroutines.CoroutineScope +import java.awt.Dimension +import java.awt.Toolkit +import java.awt.geom.RoundRectangle2D import java.nio.file.Path import kotlin.io.path.pathString import kotlin.system.exitProcess +val height = 720.dp +val width = 440.dp + + fun main() = application { val pathProvider = getPathProvider() initLogger(pathProvider.resolveLog("clipevery.log").pathString) @@ -42,6 +58,8 @@ fun main() = application { val ioScope = rememberCoroutineScope { ioDispatcher } + var showWindow by remember { mutableStateOf(false) } + val appInfo = getAppInfoFactory().createAppInfo() val dependencies = remember { @@ -54,22 +72,68 @@ fun main() = application { painterResource("clipevery_icon.png") } + val windowState = rememberWindowState( + placement = WindowPlacement.Floating, + position = WindowPosition.PlatformDefault, + size = getPreferredWindowSize(width, height) + ) + + Tray(icon = trayIcon, + mouseListener = getTrayMouseAdapter(windowState), menu = { Item( "Exit", onClick = { exitProcess(1) } ) + }, + onAction = { + logger.info { "start onAction showWindow=$showWindow" } + showWindow = !showWindow + logger.info { "end onAction showWindow=$showWindow" } } ) - Window(onCloseRequest = ::exitApplication, - title = "Clipevery", - icon = painterResource("clipevery_icon.png"), - undecorated = true, - resizable = true) { - ClipeveryApp(dependencies) + if (showWindow) { + + Window( + onCloseRequest = ::exitApplication, + state = windowState, + title = "Clipevery", + icon = painterResource("clipevery_icon.png"), + alwaysOnTop = false, + undecorated = true, + resizable = false + ) { + if (window.componentListeners.isEmpty()) { + window.addComponentListener(object : java.awt.event.ComponentAdapter() { + override fun componentResized(e: java.awt.event.ComponentEvent?) { + window.shape = RoundRectangle2D.Double( + 0.0, + 0.0, + window.width.toDouble(), + window.height.toDouble(), + 30.0, + 30.0 + ) + + } + }) + } + + ClipeveryApp(dependencies) + } } + +} + +private fun getPreferredWindowSize(desiredWidth: Dp, desiredHeight: Dp): DpSize { + val screenSize: Dimension = Toolkit.getDefaultToolkit().screenSize + val preferredWidth: Dp = (screenSize.width.dp * 0.8f) + val preferredHeight: Dp = (screenSize.height.dp * 0.8f) + val width: Dp = if (desiredWidth < preferredWidth) desiredWidth else preferredWidth + val height: Dp = if (desiredHeight < preferredHeight) desiredHeight else preferredHeight + return DpSize(width, height) } private fun getDependencies( diff --git a/composeApp/src/desktopMain/kotlin/com/clipevery/ui/TrayMouseClicked.kt b/composeApp/src/desktopMain/kotlin/com/clipevery/ui/TrayMouseClicked.kt new file mode 100644 index 000000000..ae5ed15eb --- /dev/null +++ b/composeApp/src/desktopMain/kotlin/com/clipevery/ui/TrayMouseClicked.kt @@ -0,0 +1,45 @@ +package com.clipevery.ui + +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.times +import androidx.compose.ui.window.WindowPosition +import androidx.compose.ui.window.WindowState +import com.clipevery.platform.currentPlatform +import java.awt.event.MouseAdapter + + +fun getTrayMouseAdapter(windowState: WindowState): MouseAdapter { + return if(currentPlatform().isMacos()) { + MacTrayMouseClicked(windowState) + } else { + WindowsTrayMouseClicked() + } +} + +class MacTrayMouseClicked(private val windowState: WindowState): MouseAdapter() { + + override fun mouseClicked(e: java.awt.event.MouseEvent) { + windowState.position = WindowPosition.Absolute( + x = calculatePosition(e.x.dp, windowState.size.width), + y = 32.dp + ) + } + + private fun calculatePosition(x: Dp, width: Dp): Dp { + val fNum = x / 32.dp + val iNum = fNum.toInt() + return if (fNum - iNum < 0.5f) { + iNum * 32.dp - (width / 2) + } else { + (iNum + 1) * 32.dp - (width / 2) + } + } +} + +class WindowsTrayMouseClicked: MouseAdapter() { + + override fun mouseClicked(e: java.awt.event.MouseEvent) { + + } +} \ No newline at end of file diff --git a/composeApp/src/desktopMain/kotlin/com/clipevery/utils/UIUtils.kt b/composeApp/src/desktopMain/kotlin/com/clipevery/utils/UIUtils.kt new file mode 100644 index 000000000..f1f8155aa --- /dev/null +++ b/composeApp/src/desktopMain/kotlin/com/clipevery/utils/UIUtils.kt @@ -0,0 +1,11 @@ +package com.clipevery.utils + +import androidx.compose.ui.unit.dp +import com.clipevery.model.AppUI + +fun initAppUI(): AppUI { + return AppUI( + width = 440.dp, + height = 720.dp + ) +} \ No newline at end of file From 657a175de2184010d08ed922fb0da05bc76e9fdb Mon Sep 17 00:00:00 2001 From: Yiqun Zhang Date: Thu, 23 Nov 2023 22:26:42 +0800 Subject: [PATCH 2/4] :sparkles: Determines where the interface is displayed --- .../desktopMain/kotlin/com/clipevery/main.kt | 61 ++++++++----------- 1 file changed, 24 insertions(+), 37 deletions(-) diff --git a/composeApp/src/desktopMain/kotlin/com/clipevery/main.kt b/composeApp/src/desktopMain/kotlin/com/clipevery/main.kt index 2bb3807d2..19868b01f 100644 --- a/composeApp/src/desktopMain/kotlin/com/clipevery/main.kt +++ b/composeApp/src/desktopMain/kotlin/com/clipevery/main.kt @@ -5,6 +5,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue +import androidx.compose.ui.awt.ComposeWindow import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.DpSize @@ -42,7 +43,6 @@ import java.awt.Toolkit import java.awt.geom.RoundRectangle2D import java.nio.file.Path import kotlin.io.path.pathString -import kotlin.system.exitProcess val height = 720.dp @@ -78,53 +78,40 @@ fun main() = application { size = getPreferredWindowSize(width, height) ) - Tray(icon = trayIcon, mouseListener = getTrayMouseAdapter(windowState), - menu = { - Item( - "Exit", - onClick = { exitProcess(1) } - ) - }, onAction = { - logger.info { "start onAction showWindow=$showWindow" } showWindow = !showWindow - logger.info { "end onAction showWindow=$showWindow" } } ) - if (showWindow) { - - Window( - onCloseRequest = ::exitApplication, - state = windowState, - title = "Clipevery", - icon = painterResource("clipevery_icon.png"), - alwaysOnTop = false, - undecorated = true, - resizable = false - ) { - if (window.componentListeners.isEmpty()) { - window.addComponentListener(object : java.awt.event.ComponentAdapter() { - override fun componentResized(e: java.awt.event.ComponentEvent?) { - window.shape = RoundRectangle2D.Double( - 0.0, - 0.0, - window.width.toDouble(), - window.height.toDouble(), - 30.0, - 30.0 - ) - - } - }) + Window( + onCloseRequest = ::exitApplication, + visible = showWindow, + state = windowState, + title = "Clipevery", + icon = painterResource("clipevery_icon.png"), + alwaysOnTop = true, + undecorated = true, + resizable = false + ) { + + applyRoundedCorners(window) + logger.info { "window.componentListeners = ${window.componentListeners.size}" } + window.addComponentListener(object : java.awt.event.ComponentAdapter() { + override fun componentResized(e: java.awt.event.ComponentEvent?) { + applyRoundedCorners(window) } + }) - ClipeveryApp(dependencies) - } + ClipeveryApp(dependencies) } +} +fun applyRoundedCorners(window: ComposeWindow) { + val radius = 15.0 + val shape = RoundRectangle2D.Double(0.0, 0.0, window.width.toDouble(), window.height.toDouble(), radius, radius) + window.shape = shape } private fun getPreferredWindowSize(desiredWidth: Dp, desiredHeight: Dp): DpSize { From 9e468164b6b4200d744569ce9f14d2b58d427b88 Mon Sep 17 00:00:00 2001 From: Yiqun Zhang Date: Thu, 23 Nov 2023 22:46:10 +0800 Subject: [PATCH 3/4] :sparkles: Determines where the interface is displayed --- .../desktopMain/kotlin/com/clipevery/main.kt | 29 ++++++++++++------- .../com/clipevery/ui/TrayMouseClicked.kt | 7 +++-- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/composeApp/src/desktopMain/kotlin/com/clipevery/main.kt b/composeApp/src/desktopMain/kotlin/com/clipevery/main.kt index 19868b01f..8139be4d6 100644 --- a/composeApp/src/desktopMain/kotlin/com/clipevery/main.kt +++ b/composeApp/src/desktopMain/kotlin/com/clipevery/main.kt @@ -1,5 +1,6 @@ package com.clipevery +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -79,10 +80,7 @@ fun main() = application { ) Tray(icon = trayIcon, - mouseListener = getTrayMouseAdapter(windowState), - onAction = { - showWindow = !showWindow - } + mouseListener = getTrayMouseAdapter(windowState) { showWindow = !showWindow }, ) Window( @@ -96,13 +94,22 @@ fun main() = application { resizable = false ) { - applyRoundedCorners(window) - logger.info { "window.componentListeners = ${window.componentListeners.size}" } - window.addComponentListener(object : java.awt.event.ComponentAdapter() { - override fun componentResized(e: java.awt.event.ComponentEvent?) { - applyRoundedCorners(window) - } - }) + LaunchedEffect(Unit) { + window.addComponentListener(object : java.awt.event.ComponentAdapter() { + override fun componentResized(e: java.awt.event.ComponentEvent?) { + applyRoundedCorners(window) + } + }) + window.addWindowFocusListener(object : java.awt.event.WindowFocusListener { + override fun windowGainedFocus(e: java.awt.event.WindowEvent?) { + showWindow = true + } + + override fun windowLostFocus(e: java.awt.event.WindowEvent?) { + showWindow = false + } + }) + } ClipeveryApp(dependencies) } diff --git a/composeApp/src/desktopMain/kotlin/com/clipevery/ui/TrayMouseClicked.kt b/composeApp/src/desktopMain/kotlin/com/clipevery/ui/TrayMouseClicked.kt index ae5ed15eb..baeb85d32 100644 --- a/composeApp/src/desktopMain/kotlin/com/clipevery/ui/TrayMouseClicked.kt +++ b/composeApp/src/desktopMain/kotlin/com/clipevery/ui/TrayMouseClicked.kt @@ -9,17 +9,18 @@ import com.clipevery.platform.currentPlatform import java.awt.event.MouseAdapter -fun getTrayMouseAdapter(windowState: WindowState): MouseAdapter { +fun getTrayMouseAdapter(windowState: WindowState, mouseClickedAction: () -> Unit): MouseAdapter { return if(currentPlatform().isMacos()) { - MacTrayMouseClicked(windowState) + MacTrayMouseClicked(windowState, mouseClickedAction) } else { WindowsTrayMouseClicked() } } -class MacTrayMouseClicked(private val windowState: WindowState): MouseAdapter() { +class MacTrayMouseClicked(private val windowState: WindowState, private val mouseClickedAction: () -> Unit): MouseAdapter() { override fun mouseClicked(e: java.awt.event.MouseEvent) { + mouseClickedAction() windowState.position = WindowPosition.Absolute( x = calculatePosition(e.x.dp, windowState.size.width), y = 32.dp From 9adda23bd9b4ac72bc8eedcc80a0c4a206e9629c Mon Sep 17 00:00:00 2001 From: Yiqun Zhang Date: Thu, 23 Nov 2023 23:03:59 +0800 Subject: [PATCH 4/4] :sparkles: Determines where the interface is displayed --- .../com/clipevery/ui/TrayMouseClicked.kt | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/composeApp/src/desktopMain/kotlin/com/clipevery/ui/TrayMouseClicked.kt b/composeApp/src/desktopMain/kotlin/com/clipevery/ui/TrayMouseClicked.kt index baeb85d32..884aa180f 100644 --- a/composeApp/src/desktopMain/kotlin/com/clipevery/ui/TrayMouseClicked.kt +++ b/composeApp/src/desktopMain/kotlin/com/clipevery/ui/TrayMouseClicked.kt @@ -6,14 +6,16 @@ import androidx.compose.ui.unit.times import androidx.compose.ui.window.WindowPosition import androidx.compose.ui.window.WindowState import com.clipevery.platform.currentPlatform +import java.awt.GraphicsEnvironment +import java.awt.Toolkit import java.awt.event.MouseAdapter -fun getTrayMouseAdapter(windowState: WindowState, mouseClickedAction: () -> Unit): MouseAdapter { +fun getTrayMouseAdapter(windowState: WindowState, mouseClickedAction: () -> Unit): MouseAdapter { return if(currentPlatform().isMacos()) { MacTrayMouseClicked(windowState, mouseClickedAction) } else { - WindowsTrayMouseClicked() + WindowsTrayMouseClicked(windowState, mouseClickedAction) } } @@ -38,9 +40,24 @@ class MacTrayMouseClicked(private val windowState: WindowState, private val mous } } -class WindowsTrayMouseClicked: MouseAdapter() { +class WindowsTrayMouseClicked(private val windowState: WindowState, private val mouseClickedAction: () -> Unit): MouseAdapter() { override fun mouseClicked(e: java.awt.event.MouseEvent) { + mouseClickedAction() + + val gd = GraphicsEnvironment.getLocalGraphicsEnvironment().defaultScreenDevice + val bounds = gd.defaultConfiguration.bounds + val insets = Toolkit.getDefaultToolkit().getScreenInsets(gd.defaultConfiguration) + + val usableWidth = bounds.width - insets.right + val usableHeight = bounds.height - insets.bottom + val windowWidth = windowState.size.width + val windowHeight = windowState.size.height + + windowState.position = WindowPosition.Absolute( + x = usableWidth.dp - windowWidth - 32.dp, + y = usableHeight.dp - windowHeight - 32.dp + ) } } \ No newline at end of file