diff --git a/desktop/app/build.gradle.kts b/desktop/app/build.gradle.kts index e477bf7..28e604e 100644 --- a/desktop/app/build.gradle.kts +++ b/desktop/app/build.gradle.kts @@ -65,11 +65,10 @@ dependencies { implementation(project(":desktop:tray:common")) if (Platform.getCurrentPlatform() == Platform.Desktop.Windows) { - implementation(project(":desktop:tray:windows")) + implementation(project(":desktop:tray:linux")) +// implementation(project(":desktop:tray:windows")) } else { - // TODO use external library for linux and remove this line - implementation(project(":desktop:tray:windows")) -// implementation(project(":desktop:tray:linux")) + implementation(project(":desktop:tray:linux")) } implementation(project(":shared:app")) diff --git a/desktop/tray/linux/build.gradle.kts b/desktop/tray/linux/build.gradle.kts index f877a4f..251fc46 100644 --- a/desktop/tray/linux/build.gradle.kts +++ b/desktop/tray/linux/build.gradle.kts @@ -7,4 +7,5 @@ dependencies { api(project(":desktop:tray:common")) ksp(libs.autoService.ksp) implementation(libs.autoService.annoations) + implementation(libs.systemTray) } \ No newline at end of file diff --git a/desktop/tray/linux/src/main/kotlin/ir/amirab/util/desktop/systemtray/impl/ComposeSystemTrayForLinux.kt b/desktop/tray/linux/src/main/kotlin/ir/amirab/util/desktop/systemtray/impl/ComposeSystemTrayForLinux.kt index 231c57d..685b047 100644 --- a/desktop/tray/linux/src/main/kotlin/ir/amirab/util/desktop/systemtray/impl/ComposeSystemTrayForLinux.kt +++ b/desktop/tray/linux/src/main/kotlin/ir/amirab/util/desktop/systemtray/impl/ComposeSystemTrayForLinux.kt @@ -1,14 +1,134 @@ package ir.amirab.util.desktop.systemtray.impl +import androidx.compose.runtime.* +import androidx.compose.ui.graphics.toAwtImage import com.google.auto.service.AutoService +import dorkbox.systemTray.Menu +import dorkbox.systemTray.Separator +import dorkbox.systemTray.SystemTray import ir.amirab.util.compose.IconSource import ir.amirab.util.compose.StringSource import ir.amirab.util.compose.action.MenuItem +import ir.amirab.util.desktop.GlobalDensity +import ir.amirab.util.desktop.GlobalLayoutDirection import ir.amirab.util.desktop.systemtray.IComposeSystemTray +import java.awt.Image @AutoService(IComposeSystemTray::class) class ComposeSystemTrayForLinux : IComposeSystemTray { - override fun ComposeSystemTray(icon: IconSource, title: StringSource, menu: List, onClick: () -> Unit) { - TODO("Not yet implemented") + @Composable + override fun ComposeSystemTray( + icon: IconSource, + title: StringSource, + menu: List, + onClick: () -> Unit + ) { + val systemTray: SystemTray? = remember { SystemTray.get() } + if (systemTray == null) { + System.err.println("System tray is not supported") + return + } + + val iconPainter = icon.rememberPainter() + val awtImage = remember(iconPainter) { + iconPainter.toAwtImage( + GlobalDensity, + GlobalLayoutDirection, + ) + } + val tooltipString = title.rememberString() + + LaunchedEffect(awtImage) { + systemTray.setImage(awtImage) + } + LaunchedEffect(tooltipString) { + systemTray.setTooltip(tooltipString) + } + + val immutableMenu = menu.toImmutableMenuItem() + DisposableEffect(immutableMenu) { + systemTray.menu.addImmutableMenu(immutableMenu) + onDispose { + for (entry in systemTray.menu.entries) { + systemTray.menu.remove(entry) + } + } + } + DisposableEffect(Unit) { + onDispose { + systemTray.shutdown() + } + } } +} + +private fun Menu.addImmutableMenu(immutableMenu: List) { + for (item in immutableMenu) { + when (item) { + ImmutableMenuItem.Separator -> add(Separator()) + is ImmutableMenuItem.SingleItem -> add( + dorkbox.systemTray.MenuItem( + item.title, + item.icon, + { + item.onAction() + } + ) + ) + + is ImmutableMenuItem.SubMenu -> add( + Menu( + item.title, + item.icon, + ).apply { + addImmutableMenu(item.items) + } + ) + } + } +} + +@Composable +private fun List.toImmutableMenuItem(): List { + val density = GlobalDensity + val layoutDirection = GlobalLayoutDirection + return map { + when (it) { + MenuItem.Separator -> ImmutableMenuItem.Separator + is MenuItem.SingleItem -> { + ImmutableMenuItem.SingleItem( + title = it.title.collectAsState().value.rememberString(), + icon = null, +// icon = it.icon.collectAsState().value?.rememberPainter()?.toAwtImage( +// density, layoutDirection, +// ), + onAction = it::onClick + ) + } + + is MenuItem.SubMenu -> ImmutableMenuItem.SubMenu( + title = it.title.collectAsState().value.rememberString(), + icon = it.icon.collectAsState().value?.rememberPainter()?.toAwtImage( + GlobalDensity, GlobalLayoutDirection, + ), + items = it.items.collectAsState().value.toImmutableMenuItem() + ) + } + } +} + +@Immutable +internal sealed class ImmutableMenuItem { + data object Separator : ImmutableMenuItem() + data class SingleItem( + val title: String, + val icon: Image?, + val onAction: () -> Unit, + ) : ImmutableMenuItem() + + data class SubMenu( + val title: String, + val icon: Image?, + val items: List + ) : ImmutableMenuItem() } \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 18cfbce..54dc384 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -128,6 +128,8 @@ kotlinFileWatcher = { module = "io.github.irgaly.kfswatch:kfswatch", version.ref autoService-ksp = { module = "dev.zacsweers.autoservice:auto-service-ksp", version.ref = "autoServiceKsp" } autoService-annoations = { module = "com.google.auto.service:auto-service-annotations", version.ref = "autoService" } +systemTray = "com.dorkbox:SystemTray:4.4" + [plugins] kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } compose = { id = "org.jetbrains.compose", version.ref = "compose" }