Browse Source

Update Compose

pull/942/head
Igor Demin 3 years ago committed by GitHub
parent
commit
82468294f4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 6
      README.md
  2. 0
      artwork/compose-logo.xml
  3. 2
      compose/frameworks/support
  4. 2
      templates/desktop-template/build.gradle.kts
  5. 7
      templates/desktop-template/src/main/kotlin/main.kt
  6. 2
      templates/multiplatform-template/build.gradle.kts
  7. 7
      templates/multiplatform-template/desktop/src/jvmMain/kotlin/main.kt
  8. 64
      tutorials/Desktop_Components/README.md
  9. 23
      tutorials/Getting_Started/README.md
  10. 417
      tutorials/Image_And_Icons_Manipulations/README.md
  11. BIN
      tutorials/Image_And_Icons_Manipulations/image_from_resources2.png
  12. BIN
      tutorials/Image_And_Icons_Manipulations/loading_svg_images.png
  13. BIN
      tutorials/Image_And_Icons_Manipulations/loading_xml_vector_images.png
  14. BIN
      tutorials/Image_And_Icons_Manipulations/window_icon.png
  15. 87
      tutorials/Keyboard/README.md
  16. 50
      tutorials/Mouse_Events/README.md
  17. 18
      tutorials/Native_distributions_and_local_execution/README.md
  18. 10
      tutorials/Navigation/README.md
  19. 76
      tutorials/Swing_Integration/README.md
  20. 311
      tutorials/Tray_Notifications_MenuBar/README.md
  21. BIN
      tutorials/Tray_Notifications_MenuBar/app_menubar.gif
  22. BIN
      tutorials/Tray_Notifications_MenuBar/notifier.gif
  23. BIN
      tutorials/Tray_Notifications_MenuBar/tray.gif
  24. BIN
      tutorials/Tray_Notifications_MenuBar/window_menubar.gif
  25. 91
      tutorials/Tray_Notifications_MenuBar_new/README.md
  26. 502
      tutorials/Window_API/README.md
  27. BIN
      tutorials/Window_API/center_the_window.gif
  28. BIN
      tutorials/Window_API/current_window.gif
  29. BIN
      tutorials/Window_API/focus_the_window.gif
  30. BIN
      tutorials/Window_API/scaling_factor.jpg
  31. BIN
      tutorials/Window_API/window_attr.gif
  32. BIN
      tutorials/Window_API/window_state.gif
  33. 90
      tutorials/Window_API_new/README.md

6
README.md

@ -35,10 +35,8 @@ Preview functionality (check your application UI without building/running it) fo
* [Mouse events and hover](tutorials/Mouse_Events)
* [Scrolling and scrollbars](tutorials/Desktop_Components)
* [Tooltips](tutorials/Desktop_Components#tooltips)
* [Top level windows management](tutorials/Window_API)
* [Top level windows management (new Composable API, experimental)](tutorials/Window_API_new)
* [Menu, tray, notifications](tutorials/Tray_Notifications_MenuBar)
* [Menu, tray, notifications (new Composable API, experimental)](tutorials/Tray_Notifications_MenuBar_new)
* [Top level windows management](tutorials/Window_API_new)
* [Menu, tray, notifications](tutorials/Tray_Notifications_MenuBar_new)
* [Keyboard support](tutorials/Keyboard)
* [Building native distribution](tutorials/Native_distributions_and_local_execution)
* [Signing and notarization](tutorials/Signing_and_notarization_on_macOS)

0
tutorials/Image_And_Icons_Manipulations/compose-logo.xml → artwork/compose-logo.xml

2
compose/frameworks/support

@ -1 +1 @@
Subproject commit 3de47bade63fae3a9ee0b7b5a5d9028ccaf0e3ef
Subproject commit 883fe193e46c2ad23b3aa50d4122f97a679b56ea

2
templates/desktop-template/build.gradle.kts

@ -5,7 +5,7 @@ plugins {
// __KOTLIN_COMPOSE_VERSION__
kotlin("jvm") version "1.5.21"
// __LATEST_COMPOSE_RELEASE_VERSION__
id("org.jetbrains.compose") version (System.getenv("COMPOSE_TEMPLATE_COMPOSE_VERSION") ?: "0.5.0-build262")
id("org.jetbrains.compose") version (System.getenv("COMPOSE_TEMPLATE_COMPOSE_VERSION") ?: "0.5.0-build270")
}
repositories {

7
templates/desktop-template/src/main/kotlin/main.kt

@ -1,4 +1,3 @@
import androidx.compose.desktop.Window
import androidx.compose.material.Text
import androidx.compose.material.Button
import androidx.compose.material.MaterialTheme
@ -6,8 +5,11 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
fun main() = Window {
fun main() = application {
Window(onCloseRequest = ::exitApplication) {
var text by remember { mutableStateOf("Hello, World!") }
MaterialTheme {
@ -18,3 +20,4 @@ fun main() = Window {
}
}
}
}

2
templates/multiplatform-template/build.gradle.kts

@ -1,6 +1,6 @@
buildscript {
// __LATEST_COMPOSE_RELEASE_VERSION__
val composeVersion = System.getenv("COMPOSE_TEMPLATE_COMPOSE_VERSION") ?: "0.5.0-build262"
val composeVersion = System.getenv("COMPOSE_TEMPLATE_COMPOSE_VERSION") ?: "0.5.0-build270"
repositories {
google()

7
templates/multiplatform-template/desktop/src/jvmMain/kotlin/main.kt

@ -1,5 +1,8 @@
import androidx.compose.desktop.Window
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
fun main() = Window {
fun main() = application {
Window(onCloseRequest = ::exitApplication) {
App()
}
}

64
tutorials/Desktop_Components/README.md

@ -9,7 +9,6 @@ In this tutorial, we will show you how to use desktop-specific components of Com
You can apply scrollbars to scrollable components. The scrollbar and scrollable components share a common state to synchronize with each other. For example, `VerticalScrollbar` can be attached to `Modifier.verticalScroll`, and `LazyColumnFor` and `HorizontalScrollbar` can be attached to `Modifier.horizontalScroll` and `LazyRowFor`.
```kotlin
import androidx.compose.desktop.Window
import androidx.compose.foundation.HorizontalScrollbar
import androidx.compose.foundation.VerticalScrollbar
import androidx.compose.foundation.background
@ -29,13 +28,18 @@ import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.WindowState
import androidx.compose.ui.window.singleWindowApplication
fun main() {
Window(title = "Scrollbars", size = IntSize(250, 400)) {
@OptIn(ExperimentalComposeUiApi::class)
fun main() = singleWindowApplication(
title = "Scrollbars",
state = WindowState(width = 250.dp, height = 400.dp)
) {
Box(
modifier = Modifier.fillMaxSize()
.background(color = Color(180, 180, 180))
@ -73,7 +77,6 @@ fun main() {
)
}
}
}
@Composable
fun TextBox(text: String = "Item") {
@ -96,7 +99,6 @@ fun TextBox(text: String = "Item") {
You can use scrollbars with lazy scrollable components, for example, `LazyColumn`.
```kotlin
import androidx.compose.desktop.Window
import androidx.compose.foundation.VerticalScrollbar
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
@ -112,13 +114,21 @@ import androidx.compose.foundation.rememberScrollbarAdapter
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
import androidx.compose.ui.window.rememberWindowState
fun main() {
Window(title = "Scrollbars", size = IntSize(250, 400)) {
@OptIn(ExperimentalComposeUiApi::class)
fun main() = application {
Window(
onCloseRequest = ::exitApplication,
title = "Scrollbars",
state = rememberWindowState(width = 250.dp, height = 400.dp)
) {
LazyScrollable()
}
}
@ -170,7 +180,6 @@ Scrollbars support themes to change their appearance. The example below shows ho
```kotlin
import androidx.compose.desktop.DesktopTheme
import androidx.compose.desktop.Window
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxHeight
@ -188,13 +197,21 @@ import androidx.compose.foundation.verticalScroll
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
import androidx.compose.ui.window.rememberWindowState
fun main() {
Window(title = "Scrollbars", size = IntSize(280, 400)) {
@OptIn(ExperimentalComposeUiApi::class)
fun main() = application {
Window(
onCloseRequest = ::exitApplication,
title = "Scrollbars",
state = rememberWindowState(width = 250.dp, height = 400.dp)
) {
MaterialTheme {
DesktopTheme {
Box(
@ -250,11 +267,10 @@ fun TextBox(text: String = "Item") {
You can add tooltip to any components using `BoxWithTooltip`. Basically `BoxWithTooltip` is a `Box` with the ability to show a tooltip, and has the same arguments and behavior as `Box`.
The main arguments of the `BoxWithTooltip` function:
- tooltip - composable content representing tooltip
- tooltipPlacement - describes how to place tooltip. You can specify an anchor (the mouse cursor or the component), an offset and an alignment
- delay - time delay in milliseconds after which the tooltip will be shown (default is 500 ms)
- offset - tooltip offset, the default position of the tooltip is under the mouse cursor
```kotlin
import androidx.compose.desktop.Window
import androidx.compose.foundation.BoxWithTooltip
import androidx.compose.foundation.TooltipPlacement
import androidx.compose.foundation.layout.Arrangement
@ -266,14 +282,23 @@ import androidx.compose.material.Button
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
import androidx.compose.ui.window.rememberWindowState
fun main() = Window(title = "Tooltip Example", size = IntSize(300, 300)) {
@OptIn(ExperimentalComposeUiApi::class)
fun main() = application {
Window(
onCloseRequest = ::exitApplication,
title = "Tooltip Example",
state = rememberWindowState(width = 300.dp, height = 300.dp)
) {
val buttons = listOf("Button A", "Button B", "Button C", "Button D", "Button E", "Button F")
Column(Modifier.fillMaxSize(), Arrangement.spacedBy(5.dp)) {
buttons.forEachIndexed { index, name ->
@ -305,6 +330,7 @@ fun main() = Window(title = "Tooltip Example", size = IntSize(300, 300)) {
}
}
}
}
```
![Tooltip](tooltips.gif)

23
tutorials/Getting_Started/README.md

@ -32,11 +32,11 @@ packaging JDK 15 or later must be used.
### Update the wizard plugin
The Сompose plugin version used in the wizard above may be not the last. Update the version of the plugin to the latest available by editing the `build.gradle.kts` file, finding and updating the version information as shown below. In this example the latest version of the plugin was 0.5.0-build262 and a compatible version of kotlin was 1.5.21. For the latest versions, see the [latest versions](https://github.com/JetBrains/compose-jb/releases) site and the [Kotlin](https://kotlinlang.org/) site.
The Сompose plugin version used in the wizard above may be not the last. Update the version of the plugin to the latest available by editing the `build.gradle.kts` file, finding and updating the version information as shown below. In this example the latest version of the plugin was 0.5.0-build270 and a compatible version of kotlin was 1.5.21. For the latest versions, see the [latest versions](https://github.com/JetBrains/compose-jb/releases) site and the [Kotlin](https://kotlinlang.org/) site.
```
plugins {
kotlin("jvm") version "1.5.21"
id("org.jetbrains.compose") version "0.5.0-build262"
id("org.jetbrains.compose") version "0.5.0-build270"
}
```
@ -72,7 +72,7 @@ import org.jetbrains.compose.compose
plugins {
kotlin("jvm") version "1.5.21"
id("org.jetbrains.compose") version "0.5.0-build262"
id("org.jetbrains.compose") version "0.5.0-build270"
}
repositories {
@ -92,7 +92,6 @@ compose.desktop {
```
Then create `src/main/kotlin/main.kt` and put the following code in there:
```kotlin
import androidx.compose.desktop.Window
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
@ -102,11 +101,20 @@ import androidx.compose.material.Text
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
fun main() = Window(title = "Compose for Desktop", size = IntSize(300, 300)) {
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
import androidx.compose.ui.window.rememberWindowState
@OptIn(ExperimentalComposeUiApi::class)
fun main() = application {
Window(
onCloseRequest = ::exitApplication,
title = "Compose for Desktop",
state = rememberWindowState(width = 300.dp, height = 300.dp)
) {
val count = remember { mutableStateOf(0) }
MaterialTheme {
Column(Modifier.fillMaxSize(), Arrangement.spacedBy(5.dp)) {
@ -125,6 +133,7 @@ fun main() = Window(title = "Compose for Desktop", size = IntSize(300, 300)) {
}
}
}
}
```
## Running the project

417
tutorials/Image_And_Icons_Manipulations/README.md

@ -6,128 +6,186 @@ In this tutorial we will show you how to work with images using Compose for Desk
## Loading images from resources
Using images from application resources is very simple. Suppose we have a PNG image that is placed in the `resources/images` directory in our project. For this tutorial we will use the image sample:
Using images from application resources is very simple. Suppose we have a PNG image that is placed in the `resources` directory in our project. For this tutorial we will use the image sample:
![Sample](sample.png)
```kotlin
import androidx.compose.desktop.Window
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.imageResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.window.singleWindowApplication
fun main() {
Window {
@OptIn(ExperimentalComposeUiApi::class)
fun main() = singleWindowApplication {
Image(
bitmap = imageResource("images/sample.png"), // ImageBitmap
painter = painterResource("sample.png"), // ImageBitmap
contentDescription = "Sample",
modifier = Modifier.fillMaxSize()
)
}
}
```
`painterResource` supports raster (BMP, GIF, HEIF, ICO, JPEG, PNG, WBMP, WebP) and vector formats (SVG, [XML vector drawable](https://developer.android.com/guide/topics/graphics/vector-drawable-resources)).
![Resources](image_from_resources.png)
## Loading images from device storage
## Loading images from device storage asynchronously
To create an `ImageBitmap` from a loaded image stored in the device memory you can use `org.jetbrains.skija.Image`:
To load an image stored in the device memory you can use `loadImageBitmap`, `loadSvgPainter` or `loadXmlImageVector`. The example below shows how to use them to load an image asynchronously.
```kotlin
import androidx.compose.desktop.Window
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asImageBitmap
import org.jetbrains.skija.Image
import androidx.compose.ui.graphics.painter.BitmapPainter
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.loadImageBitmap
import androidx.compose.ui.res.loadSvgPainter
import androidx.compose.ui.res.loadXmlImageVector
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.singleWindowApplication
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.xml.sax.InputSource
import java.io.File
fun main() {
Window {
val image = remember { imageFromFile(File("sample.png")) }
Image(
bitmap = image,
import java.io.IOException
@OptIn(ExperimentalComposeUiApi::class)
fun main() = singleWindowApplication {
val density = LocalDensity.current
Column {
AsyncImage(
load = { loadImageBitmap(File("sample.png")) },
painterFor = { remember { BitmapPainter(it) } },
contentDescription = "Sample",
modifier = Modifier.fillMaxSize()
modifier = Modifier.width(200.dp)
)
AsyncImage(
load = { loadSvgPainter(File("idea-logo.svg"), density) },
painterFor = { it },
contentDescription = "Idea logo",
contentScale = ContentScale.FillWidth,
modifier = Modifier.width(200.dp)
)
AsyncImage(
load = { loadXmlImageVector(File("compose-logo.xml"), density) },
painterFor = { rememberVectorPainter(it) },
contentDescription = "Compose logo",
contentScale = ContentScale.FillWidth,
modifier = Modifier.width(200.dp)
)
}
}
fun imageFromFile(file: File): ImageBitmap {
return Image.makeFromEncoded(file.readBytes()).asImageBitmap()
@Composable
fun <T> AsyncImage(
load: suspend () -> T,
painterFor: @Composable (T) -> Painter,
contentDescription: String,
modifier: Modifier = Modifier,
contentScale: ContentScale = ContentScale.Fit,
) {
val image: T? by produceState<T?>(null) {
value = withContext(Dispatchers.IO) {
try {
load()
} catch (e: IOException) {
e.printStackTrace()
null
}
}
}
if (image != null) {
Image(
painter = painterFor(image!!),
contentDescription = contentDescription,
contentScale = contentScale,
modifier = modifier
)
}
}
fun loadImageBitmap(file: File): ImageBitmap =
file.inputStream().buffered().use(::loadImageBitmap)
fun loadSvgPainter(file: File, density: Density): Painter =
file.inputStream().buffered().use { loadSvgPainter(it, density) }
fun loadXmlImageVector(file: File, density: Density): ImageVector =
file.inputStream().buffered().use { loadXmlImageVector(InputSource(it), density) }
```
![Storage](image_from_resources.png)
![Storage](image_from_resources2.png)
[PNG](sample.png)
[SVG](../../artwork/idea-logo.svg)
[XML vector drawable](../../artwork/compose-logo.xml)
## Drawing raw image data using native canvas
You may want to draw raw image data, in which case you can use `Canvas` and` drawIntoCanvas`.
```kotlin
import androidx.compose.desktop.Window
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.remember
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.Paint
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
import androidx.compose.ui.graphics.nativeCanvas
import androidx.compose.ui.res.loadImageBitmap
import androidx.compose.ui.res.useResource
import androidx.compose.ui.window.singleWindowApplication
import org.jetbrains.skija.Bitmap
import org.jetbrains.skija.ColorAlphaType
import org.jetbrains.skija.IRect
import org.jetbrains.skija.ImageInfo
import org.jetbrains.skija.Image
import java.awt.image.BufferedImage
import java.io.File
import javax.imageio.ImageIO
fun main() {
Window {
val bitmap = remember { bitmapFromByteArray() }
private val sample = useResource("sample.png", ::loadImageBitmap)
@OptIn(ExperimentalComposeUiApi::class)
fun main() = singleWindowApplication {
val bitmap = remember { bitmapFromByteArray(sample.getBytes(), sample.width, sample.height) }
Canvas(
modifier = Modifier.fillMaxSize()
) {
drawIntoCanvas { canvas ->
canvas.nativeCanvas.drawImageRect(
Image.makeFromBitmap(bitmap),
IRect(0, 0, bitmap.getWidth(), bitmap.getHeight()).toRect()
)
canvas.drawImage(bitmap, Offset.Zero, Paint())
}
}
}
}
fun bitmapFromByteArray(): Bitmap {
var image: BufferedImage? = null
try {
image = ImageIO.read(File("sample.png"))
} catch (e: Exception) {
// image file does not exist
}
if (image == null) {
image = BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB)
}
val pixels = getBytes(image) // assume we only have raw pixels
// allocate and fill skija Bitmap
fun bitmapFromByteArray(pixels: ByteArray, width: Int, height: Int): ImageBitmap {
val bitmap = Bitmap()
bitmap.allocPixels(ImageInfo.makeS32(image.width, image.height, ColorAlphaType.PREMUL))
bitmap.installPixels(bitmap.getImageInfo(), pixels, (image.width * 4).toLong())
return bitmap
bitmap.allocPixels(ImageInfo.makeS32(width, height, ColorAlphaType.PREMUL))
bitmap.installPixels(bitmap.imageInfo, pixels, (width * 4).toLong())
return bitmap.asImageBitmap()
}
// creating byte array from BufferedImage
private fun getBytes(image: BufferedImage): ByteArray {
val width = image.width
val height = image.height
private fun ImageBitmap.getBytes(): ByteArray {
val buffer = IntArray(width * height)
image.getRGB(0, 0, width, height, buffer, 0, width)
readPixels(buffer)
val pixels = ByteArray(width * height * 4)
@ -150,111 +208,27 @@ private fun getBytes(image: BufferedImage): ByteArray {
## Setting the application window icon
You have 2 ways to set icon for window:
1. Via parameter in `Window` function (or in `AppWindow` constructor)
You can set the icon for the window via parameter in the `Window` function.
Note that to change the icon on the taskbar on some OS (macOs), you should change icon in [build.gradle](https://github.com/JetBrains/compose-jb/tree/sync/2021-07-23/tutorials/Native_distributions_and_local_execution#app-icon)
```kotlin
import androidx.compose.desktop.Window
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asImageBitmap
import org.jetbrains.skija.Image
import java.awt.image.BufferedImage
import java.io.ByteArrayOutputStream
import java.io.File
import javax.imageio.ImageIO
import androidx.compose.ui.draw.paint
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
fun main() {
val image = getWindowIcon()
fun main() = application {
val icon = painterResource("sample.png")
Window(
icon = image
onCloseRequest = ::exitApplication,
icon = icon
) {
val imageAsset = remember { asImageAsset(image) }
Image(
bitmap = imageAsset,
contentDescription = "Icon",
modifier = Modifier.fillMaxSize()
)
}
}
fun getWindowIcon(): BufferedImage {
var image: BufferedImage? = null
try {
image = ImageIO.read(File("sample.png"))
} catch (e: Exception) {
// image file does not exist
Box(Modifier.paint(icon).fillMaxSize())
}
if (image == null) {
image = BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB)
}
return image
}
fun asImageAsset(image: BufferedImage): ImageBitmap {
val baos = ByteArrayOutputStream()
ImageIO.write(image, "png", baos)
return Image.makeFromEncoded(baos.toByteArray()).asImageBitmap()
}
```
2. Using `setIcon()` method
```kotlin
import androidx.compose.desktop.AppManager
import androidx.compose.desktop.Window
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asImageBitmap
import org.jetbrains.skija.Image
import java.awt.image.BufferedImage
import java.io.ByteArrayOutputStream
import java.io.File
import javax.imageio.ImageIO
fun main() {
val image = getWindowIcon()
Window {
val imageAsset = remember { asImageAsset(image) }
Image(
bitmap = imageAsset,
contentDescription = "Icon",
modifier = Modifier.fillMaxSize()
)
}
AppManager.focusedWindow?.setIcon(image)
}
fun getWindowIcon(): BufferedImage {
var image: BufferedImage? = null
try {
image = ImageIO.read(File("sample.png"))
} catch (e: Exception) {
// image file does not exist
}
if (image == null) {
image = BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB)
}
return image
}
fun asImageAsset(image: BufferedImage): ImageBitmap {
val baos = ByteArrayOutputStream()
ImageIO.write(image, "png", baos)
return Image.makeFromEncoded(baos.toByteArray()).asImageBitmap()
}
```
@ -265,131 +239,32 @@ fun asImageAsset(image: BufferedImage): ImageBitmap {
You can create a tray icon for your application:
```kotlin
import androidx.compose.desktop.AppManager
import androidx.compose.desktop.Window
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.DisposableEffect
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.window.v1.MenuItem
import androidx.compose.ui.window.v1.Tray
import org.jetbrains.skija.Image
import java.awt.image.BufferedImage
import java.io.ByteArrayOutputStream
import java.io.File
import javax.imageio.ImageIO
fun main() {
val image = getWindowIcon()
Window {
DisposableEffect(Unit) {
val tray = Tray().apply {
icon(getWindowIcon())
menu(
MenuItem(
name = "Quit App",
onClick = { AppManager.exit() }
)
)
}
onDispose {
tray.remove()
}
}
val imageAsset = asImageAsset(image)
Image(
bitmap = imageAsset,
contentDescription = "Icon",
modifier = Modifier.fillMaxSize()
)
}
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.window.Tray
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
val current = AppManager.focusedWindow
if (current != null) {
current.setIcon(image)
}
}
fun getWindowIcon(): BufferedImage {
var image: BufferedImage? = null
try {
image = ImageIO.read(File("sample.png"))
} catch (e: Exception) {
// image file does not exist
}
fun main() = application {
val icon = painterResource("sample.png")
if (image == null) {
image = BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB)
Tray(
icon = icon,
menu = {
Item("Quit App", onClick = ::exitApplication)
}
return image
}
fun asImageAsset(image: BufferedImage): ImageBitmap {
val baos = ByteArrayOutputStream()
ImageIO.write(image, "png", baos)
return Image.makeFromEncoded(baos.toByteArray()).asImageBitmap()
}
```
![Tray icon](tray_icon.png)
## Loading SVG images
Suppose we have an SVG image placed in the `resources/images` directory in our project.
[SVG](../../artwork/idea-logo.svg)
```kotlin
import androidx.compose.desktop.Window
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.svgResource
fun main() {
Window {
Image(
painter = svgResource("images/idea-logo.svg"),
contentDescription = "Idea logo",
modifier = Modifier.fillMaxSize()
)
}
}
```
![Loading XML vector images](loading_svg_images.png)
## Loading XML vector images
Compose for Desktop supports XML vector images.
XML vector images come from the world of [Android](https://developer.android.com/guide/topics/graphics/vector-drawable-resources).
We implemented it on the desktop so we can use common resources in a cross-platform application.
SVG files can be converted to XML with [Android Studio](https://developer.android.com/studio/write/vector-asset-studio#svg) or with [third-party tools](https://www.google.com/search?q=svg+to+xml).
Suppose we have an XML image placed in the `resources/images` directory in our project.
[SVG example](../../artwork/compose-logo.svg)
[Converted XML](compose-logo.xml)
```kotlin
import androidx.compose.desktop.Window
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.vectorXmlResource
fun main() {
Window {
Window(onCloseRequest = ::exitApplication, icon = icon) {
Image(
imageVector = vectorXmlResource("images/compose-logo.xml"),
contentDescription = "Compose logo",
painter = icon,
contentDescription = "Icon",
modifier = Modifier.fillMaxSize()
)
}
}
```
![Loading XML vector images](loading_xml_vector_images.png)
![Tray icon](tray_icon.png)

BIN
tutorials/Image_And_Icons_Manipulations/image_from_resources2.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

BIN
tutorials/Image_And_Icons_Manipulations/loading_svg_images.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

BIN
tutorials/Image_And_Icons_Manipulations/loading_xml_vector_images.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

BIN
tutorials/Image_And_Icons_Manipulations/window_icon.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 226 KiB

After

Width:  |  Height:  |  Size: 52 KiB

87
tutorials/Keyboard/README.md

@ -22,25 +22,27 @@ It works the same as Compose for Android, for details see [API Reference](https:
The most common use case is to define keyboard handlers for active controls like `TextField`. You can use both `onKeyEvent` and `onPreviewKeyEvent` but the last one is usually preferable to define shortcuts while it guarantees you that key events will not be consumed by children components. Here is an example:
```kotlin
import androidx.compose.desktop.Window
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.Text
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.TextField
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.key.*
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.isCtrlPressed
import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.onPreviewKeyEvent
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.singleWindowApplication
@OptIn(ExperimentalComposeUiApi::class)
fun main() = Window(title = "Compose for Desktop", size = IntSize(300, 300)) {
fun main() = singleWindowApplication {
MaterialTheme {
var consumedText by remember { mutableStateOf(0) }
var text by remember { mutableStateOf("") }
@ -51,12 +53,12 @@ fun main() = Window(title = "Compose for Desktop", size = IntSize(300, 300)) {
onValueChange = { text = it },
modifier = Modifier.onPreviewKeyEvent {
when {
(it.isMetaPressed && it.key == Key.Minus) -> {
(it.isCtrlPressed && it.key == Key.Minus) -> {
consumedText -= text.length
text = ""
true
}
(it.isMetaPressed && it.key == Key.Equals) -> {
(it.isCtrlPressed && it.key == Key.Equals) -> {
consumedText += text.length
text = ""
true
@ -70,19 +72,15 @@ fun main() = Window(title = "Compose for Desktop", size = IntSize(300, 300)) {
}
```
Note the annotation `@OptIn(ExperimentalKeyInput::class)`. Some keys related APIs are still an experimental feature of Compose, and later API changes are possible. So it requires the use of a special annotation to emphasize the experimental nature of the code.
Note the annotation `@OptIn(ExperimentalComposeUiApi::class)`. Some keys related APIs are still an experimental feature of Compose, and later API changes are possible. So it requires the use of a special annotation to emphasize the experimental nature of the code.
![keyInputFilter](keyInputFilter.gif)
## Window-scoped events
`LocalAppWindow` instances have a `keyboard` property. It is possible to use it to define keyboard event handlers that are always active in the current window. You also can get window instance for popups. Again, you possibly want to use `onPreviewKeyEvent` here to intercept events. Here is an example:
`Window`,`singleWindowApplication` and `Dialog` functions have a `onPreviewKeyEvent` and a `onKeyEvent` properties. It is possible to use them to define keyboard event handlers that are always active in the current window. You possibly want to use `onPreviewKeyEvent` here to intercept events. Here is an example:
``` kotlin
import androidx.compose.desktop.AppWindow
import androidx.compose.desktop.LocalAppWindow
import androidx.compose.desktop.Window
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
@ -91,32 +89,41 @@ import androidx.compose.material.Button
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.isMetaPressed
import androidx.compose.ui.input.key.KeyEventType
import androidx.compose.ui.input.key.isCtrlPressed
import androidx.compose.ui.input.key.isShiftPressed
import androidx.compose.ui.input.key.key
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.input.key.type
import androidx.compose.ui.unit.dp
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.singleWindowApplication
private var cleared by mutableStateOf(false)
@OptIn(ExperimentalComposeUiApi::class)
fun main() = Window(title = "Compose for Desktop", size = IntSize(300, 300)) {
MaterialTheme {
var cleared by remember { mutableStateOf(false) }
LocalAppWindow.current.keyboard.onKeyEvent = {
if (it.isMetaPressed && it.isShiftPressed && it.key == Key.C) {
fun main() = singleWindowApplication(
onKeyEvent = {
if (
it.isCtrlPressed &&
it.isShiftPressed &&
it.key == Key.C &&
it.type == KeyEventType.KeyDown
) {
cleared = true
true
} else {
false
}
}
) {
MaterialTheme {
if (cleared) {
Text("The App was cleared!")
} else {
@ -128,25 +135,29 @@ fun main() = Window(title = "Compose for Desktop", size = IntSize(300, 300)) {
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun App() {
Column(Modifier.fillMaxSize(), Arrangement.spacedBy(5.dp)) {
Button(
modifier = Modifier.padding(4.dp),
onClick = {
AppWindow(size = IntSize(200, 200)).also { window ->
window.keyboard.onPreviewKeyEvent = {
if (it.key == Key.Escape) {
window.close()
var isDialogOpen by remember { mutableStateOf(false) }
if (isDialogOpen) {
Dialog(
onCloseRequest = { isDialogOpen = false },
onPreviewKeyEvent = {
if (it.key == Key.Escape && it.type == KeyEventType.KeyDown) {
isDialogOpen = false
true
} else {
false
}
}
}.show {
Text("I'm popup!")
}) {
Text("I'm dialog!")
}
}
Column(Modifier.fillMaxSize(), Arrangement.spacedBy(5.dp)) {
Button(
modifier = Modifier.padding(4.dp),
onClick = { isDialogOpen = true }
) {
Text("Open popup")
Text("Open dialog")
}
}
}

50
tutorials/Mouse_Events/README.md

@ -13,23 +13,27 @@ Click listeners are available in both Compose on Android and Compose for Desktop
so code like this will work on both platforms:
```kotlin
import androidx.compose.desktop.Window
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material.Text
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.singleWindowApplication
fun main() = Window(title = "Compose for Desktop", size = IntSize(400, 400)) {
@OptIn(ExperimentalComposeUiApi::class)
fun main() = singleWindowApplication {
var count by remember { mutableStateOf(0) }
Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxWidth()) {
var text by remember { mutableStateOf("Click magenta box!") }
@ -67,20 +71,23 @@ the following code will only work with Compose for Desktop.
Let's create a window and install a pointer move filter on it that changes the background
color according to the mouse pointer position:
```kotlin
import androidx.compose.desktop.Window
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerMoveFilter
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.window.singleWindowApplication
fun main() = Window(title = "Compose for Desktop", size = IntSize(400, 400)) {
@OptIn(ExperimentalComposeUiApi::class)
fun main() = singleWindowApplication {
var color by remember { mutableStateOf(Color(0, 0, 0)) }
Box(
modifier = Modifier
@ -103,23 +110,26 @@ fun main() = Window(title = "Compose for Desktop", size = IntSize(400, 400)) {
Compose for Desktop also supports pointer enter and exit handlers, like this:
```kotlin
import androidx.compose.desktop.Window
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material.Text
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerMoveFilter
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.singleWindowApplication
fun main() = Window(title = "Compose for Desktop", size = IntSize(400, 400)) {
@OptIn(ExperimentalComposeUiApi::class)
fun main() = singleWindowApplication {
Column(
Modifier.background(Color.White),
verticalArrangement = Arrangement.spacedBy(10.dp)
@ -155,19 +165,19 @@ fun main() = Window(title = "Compose for Desktop", size = IntSize(400, 400)) {
Compose for Desktop contains desktop-only `Modifier.mouseClickable`, where data about pressed mouse buttons and keyboard modifiers is available. This is an experimental API, which means that it's likely to be changed before release.
```kotlin
import androidx.compose.desktop.Window
import androidx.compose.foundation.ExperimentalDesktopApi
import androidx.compose.foundation.mouseClickable
import androidx.compose.material.Text
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.window.singleWindowApplication
@OptIn(ExperimentalDesktopApi::class)
fun main() = Window(title = "Compose for Desktop", size = IntSize(400, 400)) {
@OptIn(ExperimentalComposeUiApi::class, ExperimentalDesktopApi::class)
fun main() = singleWindowApplication {
var clickableText by remember { mutableStateOf("Click me!") }
Text(

18
tutorials/Native_distributions_and_local_execution/README.md

@ -459,24 +459,28 @@ val macExtraPlistKeys: String
``` kotlin
// src/main/main.kt
import androidx.compose.desktop.Window
import androidx.compose.material.Text
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.window.singleWindowApplication
import java.awt.Desktop
@OptIn(ExperimentalComposeUiApi::class)
fun main() {
val text = mutableStateOf("Hello, World!")
var text by mutableStateOf("Hello, World!")
try {
Desktop.getDesktop().setOpenURIHandler { event ->
text.value = "Open URI: " + event.uri
text = "Open URI: " + event.uri
}
} catch (e: UnsupportedOperationException) {
println("setOpenURIHandler is unsupported")
}
Window {
var text by remember { text }
singleWindowApplication {
MaterialTheme {
Text(text)
}

10
tutorials/Navigation/README.md

@ -289,16 +289,19 @@ Application and Root initialisation:
``` kotlin
import androidx.compose.desktop.DesktopTheme
import androidx.compose.desktop.Window
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.window.singleWindowApplication
import com.arkivanov.decompose.extensions.compose.jetbrains.rememberRootComponent
fun main() {
Window("Navigation tutorial") {
@OptIn(ExperimentalComposeUiApi::class)
fun main() = singleWindowApplication(
title = "Navigation tutorial"
) {
Surface(modifier = Modifier.fillMaxSize()) {
MaterialTheme {
DesktopTheme {
@ -307,7 +310,6 @@ fun main() {
}
}
}
}
@Composable
private fun root(): Root =

76
tutorials/Swing_Integration/README.md

@ -9,34 +9,31 @@ In this tutorial, we will show you how to use ComposePanel and SwingPanel in you
ComposePanel lets you create a UI using Compose for Desktop in a Swing-based UI. To achieve this you need to create an instance of ComposePanel, add it to your Swing layout, and describe the composition inside `setContent`. You may also need to clear the CFD application events via `AppManager.setEvents`.
```kotlin
import androidx.compose.desktop.AppManager
import androidx.compose.desktop.ComposePanel
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.width
import androidx.compose.material.Text
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Button
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Alignment
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.Modifier
import androidx.compose.ui.awt.ComposePanel
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import java.awt.BorderLayout
import java.awt.Dimension
import java.awt.event.ActionEvent
import java.awt.event.ActionListener
import javax.swing.JFrame
import javax.swing.JButton
import javax.swing.JFrame
import javax.swing.SwingUtilities
import javax.swing.WindowConstants
@ -44,17 +41,7 @@ val northClicks = mutableStateOf(0)
val westClicks = mutableStateOf(0)
val eastClicks = mutableStateOf(0)
fun main() {
// explicitly clear the application events
AppManager.setEvents(
onAppStart = null,
onAppExit = null,
onWindowsEmpty = null
)
SwingComposeWindow()
}
fun SwingComposeWindow() = SwingUtilities.invokeLater {
fun main() = SwingUtilities.invokeLater {
val window = JFrame()
// creating ComposePanel
@ -62,10 +49,9 @@ fun SwingComposeWindow() = SwingUtilities.invokeLater {
window.defaultCloseOperation = WindowConstants.EXIT_ON_CLOSE
window.title = "SwingComposeWindow"
window.contentPane.add(actionButton("NORTH", { northClicks.value++ }), BorderLayout.NORTH)
window.contentPane.add(actionButton("WEST", { westClicks.value++ }), BorderLayout.WEST)
window.contentPane.add(actionButton("EAST", { eastClicks.value++ }), BorderLayout.EAST)
window.contentPane.add(actionButton("NORTH", action = { northClicks.value++ }), BorderLayout.NORTH)
window.contentPane.add(actionButton("WEST", action = { westClicks.value++ }), BorderLayout.WEST)
window.contentPane.add(actionButton("EAST", action = { eastClicks.value++ }), BorderLayout.EAST)
window.contentPane.add(
actionButton(
text = "SOUTH/REMOVE COMPOSE",
@ -85,19 +71,14 @@ fun SwingComposeWindow() = SwingUtilities.invokeLater {
}
window.setSize(800, 600)
window.setVisible(true)
window.isVisible = true
}
fun actionButton(text: String, action: (() -> Unit)? = null): JButton {
fun actionButton(text: String, action: () -> Unit): JButton {
val button = JButton(text)
button.setToolTipText("Tooltip for $text button.")
button.setPreferredSize(Dimension(100, 100))
button.addActionListener(object : ActionListener {
public override fun actionPerformed(e: ActionEvent) {
action?.invoke()
}
})
button.toolTipText = "Tooltip for $text button."
button.preferredSize = Dimension(100, 100)
button.addActionListener { action() }
return button
}
@ -147,13 +128,11 @@ fun Counter(text: String, counter: MutableState<Int>) {
![IntegrationWithSwing](screenshot.png)
## Adding a Swing component to CFD composition using SwingPanel.
## Adding a Swing component to CFD composition using SwingPanel
SwingPanel lets you create a UI using Swing in a Compose-based UI. To achieve this you need to create Swing `JComponent` in the `factory` parameter of `SwingPanel`.
```kotlin
import androidx.compose.desktop.SwingPanel
import androidx.compose.desktop.Window
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
@ -168,19 +147,19 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.awt.SwingPanel
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.singleWindowApplication
import java.awt.Component
import java.awt.Dimension
import java.awt.event.ActionEvent
import java.awt.event.ActionListener
import javax.swing.BoxLayout
import javax.swing.JButton
import javax.swing.JPanel
fun main() {
Window {
@OptIn(ExperimentalComposeUiApi::class)
fun main() = singleWindowApplication {
val counter = remember { mutableStateOf(0) }
val inc: () -> Unit = { counter.value++ }
@ -208,7 +187,7 @@ fun main() {
modifier = Modifier.size(270.dp, 90.dp),
factory = {
JPanel().apply {
setLayout(BoxLayout(this, BoxLayout.Y_AXIS))
layout = BoxLayout(this, BoxLayout.Y_AXIS)
add(actionButton("1. Swing Button: decrement", dec))
add(actionButton("2. Swing Button: decrement", dec))
add(actionButton("3. Swing Button: decrement", dec))
@ -221,7 +200,6 @@ fun main() {
}
}
}
}
@Composable
fun Button(text: String = "", action: (() -> Unit)? = null) {
@ -235,15 +213,11 @@ fun Button(text: String = "", action: (() -> Unit)? = null) {
fun actionButton(
text: String,
action: (() -> Unit)? = null
action: () -> Unit
): JButton {
val button = JButton(text)
button.setAlignmentX(Component.CENTER_ALIGNMENT)
button.addActionListener(object : ActionListener {
public override fun actionPerformed(e: ActionEvent) {
action?.invoke()
}
})
button.alignmentX = Component.CENTER_ALIGNMENT
button.addActionListener { action() }
return button
}

311
tutorials/Tray_Notifications_MenuBar/README.md

@ -1,311 +0,0 @@
# Menu, tray, notifications
## What is covered
In this tutorial we'll show you how to work with the system tray, create an application menu bar and a window-specific menu bar, and send system notifications using Compose for Desktop.
## Tray
You can add an application icon to the system tray. You can also send notifications to the user using the system tray. There are 3 types of notification:
1. notify - simple notification
2. warn - warning notification
3. error - error notification
```kotlin
import androidx.compose.desktop.AppManager
import androidx.compose.desktop.Window
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.Text
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.window.v1.MenuItem
import androidx.compose.ui.window.v1.Tray
import java.awt.Color
import java.awt.image.BufferedImage
fun main() {
val count = mutableStateOf(0)
Window(
icon = getMyAppIcon()
) {
DisposableEffect(Unit) {
val tray = Tray().apply {
icon(getTrayIcon())
menu(
MenuItem(
name = "Increment value",
onClick = {
count.value++
}
),
MenuItem(
name = "Send notification",
onClick = {
notify("Notification", "Message from MyApp!")
}
),
MenuItem(
name = "Exit",
onClick = {
AppManager.exit()
}
)
)
}
onDispose {
tray.remove()
}
}
// content
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text(text = "Value: ${count.value}")
}
}
}
fun getMyAppIcon(): BufferedImage {
val size = 256
val image = BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB)
val graphics = image.createGraphics()
graphics.setColor(Color.green)
graphics.fillOval(size / 4, 0, size / 2, size)
graphics.setColor(Color.blue)
graphics.fillOval(0, size / 4, size, size / 2)
graphics.setColor(Color.red)
graphics.fillOval(size / 4, size / 4, size / 2, size / 2)
graphics.dispose()
return image
}
fun getTrayIcon(): BufferedImage {
val size = 256
val image = BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB)
val graphics = image.createGraphics()
graphics.setColor(Color.orange)
graphics.fillOval(0, 0, size, size)
graphics.dispose()
return image
}
```
![Tray](tray.gif)
## Notifier
You can send system notifications with Notifier without using the system tray.
Notifier also has 3 types of notification:
1. notify - simple notification
2. warn - warning notification
3. error - error notification
```kotlin
import androidx.compose.desktop.Window
import androidx.compose.foundation.layout.Column
import androidx.compose.material.Text
import androidx.compose.material.Button
import androidx.compose.ui.window.Notifier
import java.awt.Color
import java.awt.image.BufferedImage
fun main() {
val message = "Some message!"
val notifier = Notifier()
Window(
icon = getMyAppIcon()
) {
Column {
Button(onClick = { notifier.notify("Notification.", message) }) {
Text(text = "Notify")
}
Button(onClick = { notifier.warn("Warning.", message) }) {
Text(text = "Warning")
}
Button(onClick = { notifier.error("Error.", message) }) {
Text(text = "Error")
}
}
}
}
fun getMyAppIcon() : BufferedImage {
val size = 256
val image = BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB)
val graphics = image.createGraphics()
graphics.setColor(Color.green)
graphics.fillOval(size / 4, 0, size / 2, size)
graphics.setColor(Color.blue)
graphics.fillOval(0, size / 4, size, size / 2)
graphics.setColor(Color.red)
graphics.fillOval(size / 4, size / 4, size / 2, size / 2)
graphics.dispose()
return image
}
```
![Notifier](notifier.gif)
## MenuBar
MenuBar is used to create and customize the common context menu of the application or a particular window.
To create a common context menu for all the application windows, you need to configure the AppManager.
```kotlin
import androidx.compose.desktop.AppManager
import androidx.compose.desktop.Window
import androidx.compose.material.Text
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.Modifier
import androidx.compose.ui.window.v1.KeyStroke
import androidx.compose.ui.window.v1.MenuItem
import androidx.compose.ui.window.v1.Menu
import androidx.compose.ui.window.v1.MenuBar
@OptIn(ExperimentalComposeUiApi::class)
fun main() {
// To use Apple global menu.
System.setProperty("apple.laf.useScreenMenuBar", "true")
val action = mutableStateOf("Last action: None")
AppManager.setMenu(
MenuBar(
Menu(
name = "Actions",
MenuItem(
name = "About",
onClick = { action.value = "Last action: About (Command + I)" },
shortcut = KeyStroke(Key.I)
),
MenuItem(
name = "Exit",
onClick = { AppManager.exit() },
shortcut = KeyStroke(Key.X)
)
),
Menu(
name = "File",
MenuItem(
name = "Copy",
onClick = { action.value = "Last action: Copy (Command + C)" },
shortcut = KeyStroke(Key.C)
),
MenuItem(
name = "Paste",
onClick = { action.value = "Last action: Paste (Command + V)" },
shortcut = KeyStroke(Key.V)
)
)
)
)
Window {
// content
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text(text = action.value)
}
}
}
```
![Application MenuBar](app_menubar.gif)
You can create a MenuBar for a specific window, and have the other windows use the defined MenuBar.
```kotlin
import androidx.compose.desktop.AppManager
import androidx.compose.desktop.Window
import androidx.compose.material.Text
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.Button
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.Modifier
import androidx.compose.ui.window.v1.KeyStroke
import androidx.compose.ui.window.v1.MenuItem
import androidx.compose.ui.window.v1.Menu
import androidx.compose.ui.window.v1.MenuBar
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
@OptIn(ExperimentalComposeUiApi::class)
fun main() {
// To use Apple global menu.
System.setProperty("apple.laf.useScreenMenuBar", "true")
val action = mutableStateOf("Last action: None")
Window(
menuBar = MenuBar(
Menu(
name = "Actions",
MenuItem(
name = "About",
onClick = { action.value = "Last action: About (Command + I)" },
shortcut = KeyStroke(Key.I)
),
MenuItem(
name = "Exit",
onClick = { AppManager.exit() },
shortcut = KeyStroke(Key.X)
)
),
Menu(
name = "File",
MenuItem(
name = "Copy",
onClick = { action.value = "Last action: Copy (Command + C)" },
shortcut = KeyStroke(Key.C)
),
MenuItem(
name = "Paste",
onClick = { action.value = "Last action: Paste (Command + V)" },
shortcut = KeyStroke(Key.V)
)
)
)
) {
// content
Button(
onClick = {
Window(
title = "Another window",
size = IntSize(350, 200),
location = IntOffset(100, 100),
centered = false
) {
}
}
) {
Text(text = "New window")
}
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text(text = action.value)
}
}
}
```
![Window MenuBar](window_menubar.gif)

BIN
tutorials/Tray_Notifications_MenuBar/app_menubar.gif

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 MiB

BIN
tutorials/Tray_Notifications_MenuBar/notifier.gif

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 MiB

BIN
tutorials/Tray_Notifications_MenuBar/tray.gif

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 MiB

BIN
tutorials/Tray_Notifications_MenuBar/window_menubar.gif

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 MiB

91
tutorials/Tray_Notifications_MenuBar_new/README.md

@ -16,22 +16,23 @@ You can add an application icon to the system tray. You can also send notificati
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.Text
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.window.Notification
import androidx.compose.ui.window.Tray
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
import androidx.compose.ui.window.rememberTrayState
import java.awt.Color
import java.awt.image.BufferedImage
@OptIn(ExperimentalComposeUiApi::class)
fun main() = application {
var count by remember { mutableStateOf(0) }
var isOpen by remember { mutableStateOf(true) }
@ -39,13 +40,13 @@ fun main() = application {
if (isOpen) {
Window(
onCloseRequest = ::exitApplication,
icon = remember { getMyAppIcon() }
icon = MyAppIcon
) {
val trayState = rememberTrayState()
val notification = Notification("Notification", "Message from MyApp!")
Tray(
state = trayState,
icon = remember { getTrayIcon() },
icon = TrayIcon,
menu = {
Item(
"Increment value",
@ -73,34 +74,28 @@ fun main() = application {
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text(text = "Value: ${count}")
Text(text = "Value: $count")
}
}
}
}
fun getMyAppIcon(): BufferedImage {
val size = 256
val image = BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB)
val graphics = image.createGraphics()
graphics.color = Color.green
graphics.fillOval(size / 4, 0, size / 2, size)
graphics.color = Color.blue
graphics.fillOval(0, size / 4, size, size / 2)
graphics.color = Color.red
graphics.fillOval(size / 4, size / 4, size / 2, size / 2)
graphics.dispose()
return image
object MyAppIcon : Painter() {
override val intrinsicSize = Size(256f, 256f)
override fun DrawScope.onDraw() {
drawOval(Color.Green, Offset(size.width / 4, 0f), Size(size.width / 2f, size.height))
drawOval(Color.Blue, Offset(0f, size.height / 4), Size(size.width, size.height / 2f))
drawOval(Color.Red, Offset(size.width / 4, size.height / 4), Size(size.width / 2f, size.height / 2f))
}
}
fun getTrayIcon(): BufferedImage {
val size = 256
val image = BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB)
val graphics = image.createGraphics()
graphics.color = Color.orange
graphics.fillOval(0, 0, size, size)
graphics.dispose()
return image
object TrayIcon : Painter() {
override val intrinsicSize = Size(256f, 256f)
override fun DrawScope.onDraw() {
drawOval(Color(0xFFFFA500))
}
}
```
@ -121,16 +116,18 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.KeyShortcut
import androidx.compose.ui.window.MenuBar
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
@OptIn(ExperimentalComposeUiApi::class)
fun main() {
// Currently we use Swing's menu under the hood, so we need to set this property to change the look and feel of the menu on Windows/Linux
System.setProperty("skiko.rendering.laf.global", "true")
application {
fun main() = application {
var action by remember { mutableStateOf("Last action: None") }
var isOpen by remember { mutableStateOf(true) }
@ -139,10 +136,15 @@ fun main() {
Window(onCloseRequest = { isOpen = false }) {
MenuBar {
Menu("Actions") {
Item(
if (isSubmenuShowing) "Hide advanced settings" else "Show advanced settings",
onClick = {
Menu("File", mnemonic = 'F') {
Item("Copy", onClick = { action = "Last action: Copy" }, shortcut = KeyShortcut(Key.C, ctrl = true))
Item("Paste", onClick = { action = "Last action: Paste" }, shortcut = KeyShortcut(Key.V, ctrl = true))
}
Menu("Actions", mnemonic = 'A') {
CheckboxItem(
"Advanced settings",
checked = isSubmenuShowing,
onCheckedChange = {
isSubmenuShowing = !isSubmenuShowing
}
)
@ -153,12 +155,8 @@ fun main() {
}
}
Separator()
Item("About", onClick = { action = "Last action: About" })
Item("Exit", onClick = { isOpen = false })
}
Menu("File") {
Item("Copy", onClick = { action = "Last action: Copy" })
Item("Paste", onClick = { action = "Last action: Paste" },)
Item("About", icon = TrayIcon, onClick = { action = "Last action: About" })
Item("Exit", onClick = { isOpen = false }, shortcut = KeyShortcut(Key.Escape), mnemonic = 'E')
}
}
@ -171,6 +169,13 @@ fun main() {
}
}
}
object TrayIcon : Painter() {
override val intrinsicSize = Size(256f, 256f)
override fun DrawScope.onDraw() {
drawOval(Color(0xFFFFA500))
}
}
```

502
tutorials/Window_API/README.md

@ -1,502 +0,0 @@
# Top level windows management
## What is covered
In this tutorial we will show you how to work with windows using Compose for Desktop.
## Windows creation
The main class for creating windows is AppWindow. The easiest way to create and launch a new window is to use an instance of the AppWindow class and call its method `show()`. You can see an example below:
```kotlin
import androidx.compose.desktop.AppWindow
import javax.swing.SwingUtilities.invokeLater
fun main() = invokeLater {
AppWindow().show {
// Content
}
}
```
Note that AppWindow should be created in AWT Event Thread. Instead of calling `invokeLater()` explicitly you can use `Window` DSL:
```kotlin
import androidx.compose.desktop.Window
fun main() {
Window {
// Content
}
}
```
There are two types of window – modal and regular. Below are the functions for creating each type of window:
1. Window – regular window type.
2. Dialog – modal window type. Such a window locks its parent window until the user completes working with it and closes the modal window.
You can see an example of both types of window below.
```kotlin
import androidx.compose.desktop.Window
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.window.v1.Dialog
fun main() {
Window {
val dialogState = remember { mutableStateOf(false) }
Button(onClick = { dialogState.value = true }) {
Text(text = "Open dialog")
}
if (dialogState.value) {
Dialog(
onDismissRequest = { dialogState.value = false }
) {
// Dialog's content
}
}
}
}
```
## Window attributes
Each window has following parameters, all of them could be omitted and have default values:
1. title – window title
2. size – initial window size
3. location – initial window position
4. centered – set the window to the center of the display
5. icon – window icon
6. menuBar – window context menu
7. undecorated – disable native border and title bar of the window
8. resizable – makes the window resizable or unresizable
9. events – window events
10. onDismissEvent – event when removing the window content from a composition
An example of using window parameters in the creation step:
```kotlin
import androidx.compose.desktop.AppManager
import androidx.compose.desktop.Window
import androidx.compose.desktop.WindowEvents
import androidx.compose.material.Text
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.Button
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.window.v1.MenuItem
import androidx.compose.ui.window.v1.KeyStroke
import androidx.compose.ui.window.v1.Menu
import androidx.compose.ui.window.v1.MenuBar
import java.awt.Color
import java.awt.image.BufferedImage
@OptIn(ExperimentalComposeUiApi::class)
fun main() {
val count = mutableStateOf(0)
val windowPos = mutableStateOf(IntOffset.Zero)
Window(
title = "MyApp",
size = IntSize(400, 250),
location = IntOffset(100, 100),
centered = false, // true - by default
icon = getMyAppIcon(),
menuBar = MenuBar(
Menu(
name = "Actions",
MenuItem(
name = "Increment value",
onClick = {
count.value++
},
shortcut = KeyStroke(Key.I)
),
MenuItem(
name = "Exit",
onClick = { AppManager.exit() },
shortcut = KeyStroke(Key.X)
)
)
),
undecorated = true, // false - by default
events = WindowEvents(
onRelocate = { location ->
windowPos.value = location
}
)
) {
// content
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Column {
Text(text = "Location: ${windowPos.value} Value: ${count.value}")
Button(
onClick = {
AppManager.exit()
}
) {
Text(text = "Close app")
}
}
}
}
}
fun getMyAppIcon() : BufferedImage {
val size = 256
val image = BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB)
val graphics = image.createGraphics()
graphics.color = Color.orange
graphics.fillOval(0, 0, size, size)
graphics.dispose()
return image
}
```
![Window attributes](window_attr.gif)
## Window properties
AppWindow parameters correspond to the following properties:
1. title – window title
2. width – window width
3. height – window height
4. x – position of the left top corner of the window along the X axis
5. y – position of the left top corner of the window along the Y axis
6. resizable - returns `true` if the window resizable, `false` otherwise
7. icon – window icon image
8. events – window events
To get the properties of a window, it is enough to have a link to the current or specific window. There are two ways to get the current focused window:
1. Using the global environment:
```kotlin
import androidx.compose.desktop.LocalAppWindow
import androidx.compose.desktop.Window
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.IntOffset
fun main() {
val windowPos = mutableStateOf(IntOffset.Zero)
Window {
val current = LocalAppWindow.current
// Content
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Column {
Text(text = "Location: ${windowPos.value}")
Button(
onClick = {
windowPos.value = IntOffset(current.x, current.y)
}
) {
Text(text = "Print window location")
}
}
}
}
}
```
2. Using AppManager:
```kotlin
import androidx.compose.desktop.AppManager
import androidx.compose.desktop.Window
import androidx.compose.material.Text
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.Button
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.IntOffset
fun main() {
val windowPos = mutableStateOf(IntOffset.Zero)
Window {
// Content
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Column {
Text(text = "Location: ${windowPos.value}")
Button(
onClick = {
val current = AppManager.focusedWindow
if (current != null) {
windowPos.value = IntOffset(current.x, current.y)
}
}
) {
Text(text = "Print window location")
}
}
}
}
}
```
![Window properties](current_window.gif)
Using the following methods, you can change the properties of the AppWindow:
1. setTitle(title: String) – window title
2. setSize(width: Int, height: Int) – window size
3. setLocation(x: Int, y: Int) – window position
4. setWindowCentered() – set the window to the center of the display
5. setIcon(image: BufferedImage?) – window icon
6. setMenuBar(menuBar: MenuBar) - window menu bar
```kotlin
import androidx.compose.desktop.LocalAppWindow
import androidx.compose.desktop.Window
import androidx.compose.material.Text
import androidx.compose.material.Button
fun main() {
Window {
val window = LocalAppWindow.current
// Content
Button(
onClick = {
window.setWindowCentered()
}
) {
Text(text = "Center the window")
}
}
}
```
![Window properties](center_the_window.gif)
## Methods
Using the following methods, you can change the state of the AppWindow:
1. show(parentComposition: CompositionReference? = null, content: @Composable () -> Unit) – shows a window with the given Compose content,
`parentComposition` is the parent of this window's composition.
2. close() - closes the window.
3. minimize() - minimizes the window to the taskbar. If the window is in fullscreen mode this method is ignored.
4. maximize() - maximizes the window to fill all available screen space. If the window is in fullscreen mode this method is ignored.
5. makeFullscreen() - switches the window to fullscreen mode if the window is resizable. If the window is in fullscreen mode `minimize()` and `maximize()` methods are ignored.
6. restore() - restores the normal state and size of the window after maximizing/minimizing/fullscreen mode.
You can know about window state via properties below:
1. isMinimized - returns true if the window is minimized, false otherwise.
2. isMaximized - returns true if the window is maximized, false otherwise.
3. isFullscreen - returns true if the window is in fullscreen state, false otherwise.
```kotlin
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.Spacer
import androidx.compose.desktop.AppManager
import androidx.compose.desktop.AppWindow
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import javax.swing.SwingUtilities.invokeLater
fun main() = invokeLater {
AppWindow().show {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Column(
modifier = Modifier.padding(top = 20.dp, bottom = 20.dp)
) {
Button("Minimize", { AppManager.focusedWindow?.minimize() })
Button("Maximize", { AppManager.focusedWindow?.maximize() })
Button("Fullscreen", { AppManager.focusedWindow?.makeFullscreen() })
Button("Restore", { AppManager.focusedWindow?.restore() })
Spacer(modifier = Modifier.height(20.dp))
Button("Close", { AppManager.focusedWindow?.close() })
}
}
}
}
@Composable
fun Button(text: String = "", action: (() -> Unit)? = null) {
Button(
modifier = Modifier.size(150.dp, 30.dp),
onClick = { action?.invoke() }
) {
Text(text)
}
Spacer(modifier = Modifier.height(10.dp))
}
```
![Window state](window_state.gif)
## Window events
Events can be defined using the events parameter in the window creation step or redefine using the events property at runtime.
Actions can be assigned to the following window events:
1. onOpen – event during window opening
2. onClose – event during window closing
3. onMinimize – event during window minimizing
4. onMaximize – event during window maximizing
5. onRestore – event during restoring window size after window minimize/maximize
6. onFocusGet – event when window gets focus
7. onFocusLost – event when window loses focus
8. onResize – event on window resize (argument is window size as IntSize)
9. onRelocate – event of the window reposition on display (argument is window position as IntOffset)
```kotlin
import androidx.compose.desktop.Window
import androidx.compose.desktop.WindowEvents
import androidx.compose.material.Text
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.IntSize
fun main() {
val windowSize = mutableStateOf(IntSize.Zero)
val focused = mutableStateOf(false)
Window(
events = WindowEvents(
onFocusGet = { focused.value = true },
onFocusLost = { focused.value = false },
onResize = { size ->
windowSize.value = size
}
)
) {
// Content
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text(text = "Size: ${windowSize.value} Focused: ${focused.value}")
}
}
}
```
![Window events](focus_the_window.gif)
## AppManager
The AppManager singleton is used to customize the behavior of the entire application. Its main features:
1. Description of common application events
``` kotlin
AppManager.setEvents(
onAppStart = { println("onAppStart") }, // Invoked before the first window is created
onAppExit = { println("onAppExit") } // Invoked after all windows are closed
)
```
2. Customization of common application context menu
``` kotlin
AppManager.setMenu(
getCommonAppMenuBar() // Custom function that returns MenuBar
)
```
3. Access to the application windows list
``` kotlin
val windows = AppManager.windows
```
4. Getting the current focused window
``` kotlin
val current = AppManager.focusedWindow
```
5. Application exit
``` kotlin
AppManager.exit() // Closes all windows
```
## Access to Swing components
Compose for Desktop is tightly integrated with Swing at the top-level windows layer. For more detailed customization, you can access the JFrame class:
```kotlin
import androidx.compose.desktop.AppManager
import androidx.compose.desktop.Window
import androidx.compose.material.Text
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.Button
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
fun main() {
val scaleFactor = mutableStateOf(0.0)
Window {
// Content
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Column {
Button(
onClick = {
val current = AppManager.focusedWindow
if (current != null) {
val jFrame = current.window
// Do whatever you want with it
scaleFactor.value = jFrame.graphicsConfiguration.defaultTransform.scaleX
}
}
) {
Text(text = "Check display scaling factor")
}
Text(text = "Scaling factor: ${scaleFactor.value}")
}
}
}
}
```
![Access to Swing components](scaling_factor.jpg)

BIN
tutorials/Window_API/center_the_window.gif

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 MiB

BIN
tutorials/Window_API/current_window.gif

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 MiB

BIN
tutorials/Window_API/focus_the_window.gif

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 MiB

BIN
tutorials/Window_API/scaling_factor.jpg

Binary file not shown.

Before

Width:  |  Height:  |  Size: 209 KiB

BIN
tutorials/Window_API/window_attr.gif

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 MiB

BIN
tutorials/Window_API/window_state.gif

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 MiB

90
tutorials/Window_API_new/README.md

@ -13,11 +13,9 @@ Top-level windows can be conditionally created in other composable functions and
The main function for creating windows is `Window`. This function should be used in a Composable scope. The easiest way to create a Composable scope is to use the `application` function:
```kotlin
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
@OptIn(ExperimentalComposeUiApi::class)
fun main() = application {
Window(onCloseRequest = ::exitApplication) {
// Content
@ -33,11 +31,9 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
@OptIn(ExperimentalComposeUiApi::class)
fun main() = application {
var fileName by remember { mutableStateOf("Untitled") }
@ -61,18 +57,18 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
import kotlinx.coroutines.delay
@OptIn(ExperimentalComposeUiApi::class)
fun main() = application {
var isPerformingTask by remember { mutableStateOf(true) }
LaunchedEffect(Unit) {
delay(2000) // Do some heavy lifting
isPerformingTask = false
}
if (isPerformingTask) {
Window(onCloseRequest = ::exitApplication) {
Text("Performing some tasks. Please wait!")
@ -96,12 +92,10 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
@OptIn(ExperimentalComposeUiApi::class)
fun main() = application {
var isOpen by remember { mutableStateOf(true) }
var isAskingToClose by remember { mutableStateOf(false) }
@ -136,16 +130,15 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.window.Tray
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
import androidx.compose.ui.window.rememberWindowState
import kotlinx.coroutines.delay
import java.awt.Color
import java.awt.image.BufferedImage
@OptIn(ExperimentalComposeUiApi::class)
fun main() = application {
var isVisible by remember { mutableStateOf(true) }
@ -166,7 +159,7 @@ fun main() = application {
if (!isVisible) {
Tray(
remember { getTrayIcon() },
TrayIcon,
hint = "Counter",
onAction = { isVisible = true },
menu = {
@ -176,14 +169,12 @@ fun main() = application {
}
}
fun getTrayIcon(): BufferedImage {
val size = 256
val image = BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB)
val graphics = image.createGraphics()
graphics.color = Color.orange
graphics.fillOval(0, 0, size, size)
graphics.dispose()
return image
object TrayIcon : Painter() {
override val intrinsicSize = Size(256f, 256f)
override fun DrawScope.onDraw() {
drawOval(Color(0xFFFFA500))
}
}
```
![](hide_instead_of_close.gif)
@ -195,11 +186,11 @@ import androidx.compose.runtime.key
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.remember
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.window.ApplicationScope
import androidx.compose.ui.window.MenuBar
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
@OptIn(ExperimentalComposeUiApi::class)
fun main() = application {
val applicationState = remember { MyApplicationState() }
@ -212,7 +203,7 @@ fun main() = application {
@OptIn(ExperimentalComposeUiApi::class)
@Composable
private fun MyWindow(
private fun ApplicationScope.MyWindow(
state: MyWindowState
) = Window(onCloseRequest = state::close, title = state.title) {
MenuBar {
@ -271,7 +262,6 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.material.Checkbox
import androidx.compose.material.Text
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Window
@ -280,7 +270,6 @@ import androidx.compose.ui.window.WindowPosition
import androidx.compose.ui.window.application
import androidx.compose.ui.window.rememberWindowState
@OptIn(ExperimentalComposeUiApi::class)
fun main() = application {
val state = rememberWindowState(placement = WindowPlacement.Maximized)
@ -344,10 +333,9 @@ fun main() = application {
## Listening the state of the window
Reading the state in composition is useful when you need to update UI, but there are cases when you need to react to the state changes and send a value to another non-composable level of your application (write it to the database, for example):
```
```kotlin
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.WindowPosition
import androidx.compose.ui.window.WindowSize
@ -357,11 +345,10 @@ import kotlinx.coroutines.flow.filterNot
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@OptIn(ExperimentalComposeUiApi::class)
fun main() = application {
val state = rememberWindowState()
Window(state) {
Window(onCloseRequest = ::exitApplication, state) {
// Content
LaunchedEffect(state) {
@ -370,7 +357,7 @@ fun main() = application {
.launchIn(this)
snapshotFlow { state.position }
.filterNot { it.isInitial }
.filterNot { it.isSpecified }
.onEach(::onWindowRelocate)
.launchIn(this)
}
@ -386,35 +373,6 @@ private fun onWindowRelocate(position: WindowPosition) {
}
```
## Handle window-level shortcuts
```kotlin
import androidx.compose.material.TextField
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.KeyEventType
import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.type
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
@OptIn(ExperimentalComposeUiApi::class)
fun main() = application {
Window(
onCloseRequest = ::exitApplication,
onPreviewKeyEvent = {
if (it.type == KeyEventType.KeyDown && it.key == Key.Escape) {
exitApplication()
true
} else {
false
}
}
) {
TextField("Text", {})
}
}
```
## Dialogs
There are two types of window – modal and regular. Below are the functions for creating each:
@ -427,19 +385,16 @@ You can see an example of both types of window below.
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogState
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.WindowPosition
import androidx.compose.ui.window.WindowState
import androidx.compose.ui.window.application
@OptIn(ExperimentalComposeUiApi::class)
fun main() = application {
Window(
onCloseRequest = ::exitApplication,
@ -465,13 +420,11 @@ fun main() = application {
## Swing interoperability
Because Compose for Desktop uses Swing under the hood, it is possible to create a window using Swing directly:
```kotlin
import androidx.compose.desktop.ComposeWindow
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.awt.ComposeWindow
import java.awt.Dimension
import javax.swing.JFrame
import javax.swing.SwingUtilities
@OptIn(ExperimentalComposeUiApi::class)
fun main() = SwingUtilities.invokeLater {
ComposeWindow().apply {
size = Dimension(300, 300)
@ -509,13 +462,11 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.window.AwtWindow
import androidx.compose.ui.window.application
import java.awt.FileDialog
import java.awt.Frame
@OptIn(ExperimentalComposeUiApi::class)
fun main() = application {
var isOpen by remember { mutableStateOf(true) }
@ -529,7 +480,6 @@ fun main() = application {
}
}
@OptIn(ExperimentalComposeUiApi::class)
@Composable
private fun FileDialog(
parent: Frame? = null,

Loading…
Cancel
Save