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. 19
      templates/desktop-template/src/main/kotlin/main.kt
  6. 2
      templates/multiplatform-template/build.gradle.kts
  7. 9
      templates/multiplatform-template/desktop/src/jvmMain/kotlin/main.kt
  8. 176
      tutorials/Desktop_Components/README.md
  9. 49
      tutorials/Getting_Started/README.md
  10. 439
      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. 103
      tutorials/Keyboard/README.md
  16. 50
      tutorials/Mouse_Events/README.md
  17. 20
      tutorials/Native_distributions_and_local_execution/README.md
  18. 18
      tutorials/Navigation/README.md
  19. 138
      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. 137
      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. 96
      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) * [Mouse events and hover](tutorials/Mouse_Events)
* [Scrolling and scrollbars](tutorials/Desktop_Components) * [Scrolling and scrollbars](tutorials/Desktop_Components)
* [Tooltips](tutorials/Desktop_Components#tooltips) * [Tooltips](tutorials/Desktop_Components#tooltips)
* [Top level windows management](tutorials/Window_API) * [Top level windows management](tutorials/Window_API_new)
* [Top level windows management (new Composable API, experimental)](tutorials/Window_API_new) * [Menu, tray, notifications](tutorials/Tray_Notifications_MenuBar_new)
* [Menu, tray, notifications](tutorials/Tray_Notifications_MenuBar)
* [Menu, tray, notifications (new Composable API, experimental)](tutorials/Tray_Notifications_MenuBar_new)
* [Keyboard support](tutorials/Keyboard) * [Keyboard support](tutorials/Keyboard)
* [Building native distribution](tutorials/Native_distributions_and_local_execution) * [Building native distribution](tutorials/Native_distributions_and_local_execution)
* [Signing and notarization](tutorials/Signing_and_notarization_on_macOS) * [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_COMPOSE_VERSION__
kotlin("jvm") version "1.5.21" kotlin("jvm") version "1.5.21"
// __LATEST_COMPOSE_RELEASE_VERSION__ // __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 { repositories {

19
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.Text
import androidx.compose.material.Button import androidx.compose.material.Button
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
@ -6,15 +5,19 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
fun main() = Window { fun main() = application {
var text by remember { mutableStateOf("Hello, World!") } Window(onCloseRequest = ::exitApplication) {
var text by remember { mutableStateOf("Hello, World!") }
MaterialTheme { MaterialTheme {
Button(onClick = { Button(onClick = {
text = "Hello, Desktop!" text = "Hello, Desktop!"
}) { }) {
Text(text) Text(text)
}
} }
} }
} }

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

@ -1,6 +1,6 @@
buildscript { buildscript {
// __LATEST_COMPOSE_RELEASE_VERSION__ // __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 { repositories {
google() google()

9
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 {
App() Window(onCloseRequest = ::exitApplication) {
App()
}
} }

176
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`. 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 ```kotlin
import androidx.compose.desktop.Window
import androidx.compose.foundation.HorizontalScrollbar import androidx.compose.foundation.HorizontalScrollbar
import androidx.compose.foundation.VerticalScrollbar import androidx.compose.foundation.VerticalScrollbar
import androidx.compose.foundation.background import androidx.compose.foundation.background
@ -29,49 +28,53 @@ import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.WindowState
import androidx.compose.ui.window.singleWindowApplication
@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))
.padding(10.dp)
) {
val stateVertical = rememberScrollState(0)
val stateHorizontal = rememberScrollState(0)
fun main() {
Window(title = "Scrollbars", size = IntSize(250, 400)) {
Box( Box(
modifier = Modifier.fillMaxSize() modifier = Modifier
.background(color = Color(180, 180, 180)) .fillMaxSize()
.padding(10.dp) .verticalScroll(stateVertical)
.padding(end = 12.dp, bottom = 12.dp)
.horizontalScroll(stateHorizontal)
) { ) {
val stateVertical = rememberScrollState(0) Column {
val stateHorizontal = rememberScrollState(0) for (item in 0..30) {
TextBox("Item #$item")
Box( if (item < 30) {
modifier = Modifier Spacer(modifier = Modifier.height(5.dp))
.fillMaxSize()
.verticalScroll(stateVertical)
.padding(end = 12.dp, bottom = 12.dp)
.horizontalScroll(stateHorizontal)
) {
Column {
for (item in 0..30) {
TextBox("Item #$item")
if (item < 30) {
Spacer(modifier = Modifier.height(5.dp))
}
} }
} }
} }
VerticalScrollbar(
modifier = Modifier.align(Alignment.CenterEnd)
.fillMaxHeight(),
adapter = rememberScrollbarAdapter(stateVertical)
)
HorizontalScrollbar(
modifier = Modifier.align(Alignment.BottomStart)
.fillMaxWidth()
.padding(end = 12.dp),
adapter = rememberScrollbarAdapter(stateHorizontal)
)
} }
VerticalScrollbar(
modifier = Modifier.align(Alignment.CenterEnd)
.fillMaxHeight(),
adapter = rememberScrollbarAdapter(stateVertical)
)
HorizontalScrollbar(
modifier = Modifier.align(Alignment.BottomStart)
.fillMaxWidth()
.padding(end = 12.dp),
adapter = rememberScrollbarAdapter(stateHorizontal)
)
} }
} }
@ -96,7 +99,6 @@ fun TextBox(text: String = "Item") {
You can use scrollbars with lazy scrollable components, for example, `LazyColumn`. You can use scrollbars with lazy scrollable components, for example, `LazyColumn`.
```kotlin ```kotlin
import androidx.compose.desktop.Window
import androidx.compose.foundation.VerticalScrollbar import androidx.compose.foundation.VerticalScrollbar
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
@ -112,13 +114,21 @@ import androidx.compose.foundation.rememberScrollbarAdapter
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp 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() { @OptIn(ExperimentalComposeUiApi::class)
Window(title = "Scrollbars", size = IntSize(250, 400)) { fun main() = application {
Window(
onCloseRequest = ::exitApplication,
title = "Scrollbars",
state = rememberWindowState(width = 250.dp, height = 400.dp)
) {
LazyScrollable() LazyScrollable()
} }
} }
@ -170,7 +180,6 @@ Scrollbars support themes to change their appearance. The example below shows ho
```kotlin ```kotlin
import androidx.compose.desktop.DesktopTheme import androidx.compose.desktop.DesktopTheme
import androidx.compose.desktop.Window
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
@ -188,13 +197,21 @@ import androidx.compose.foundation.verticalScroll
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp 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() { @OptIn(ExperimentalComposeUiApi::class)
Window(title = "Scrollbars", size = IntSize(280, 400)) { fun main() = application {
Window(
onCloseRequest = ::exitApplication,
title = "Scrollbars",
state = rememberWindowState(width = 250.dp, height = 400.dp)
) {
MaterialTheme { MaterialTheme {
DesktopTheme { DesktopTheme {
Box( 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`. 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: The main arguments of the `BoxWithTooltip` function:
- tooltip - composable content representing tooltip - 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) - 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 ```kotlin
import androidx.compose.desktop.Window
import androidx.compose.foundation.BoxWithTooltip import androidx.compose.foundation.BoxWithTooltip
import androidx.compose.foundation.TooltipPlacement import androidx.compose.foundation.TooltipPlacement
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
@ -266,41 +282,51 @@ import androidx.compose.material.Button
import androidx.compose.material.Surface import androidx.compose.material.Surface
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.ui.Alignment 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.draw.shadow
import androidx.compose.ui.graphics.Color 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.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)
val buttons = listOf("Button A", "Button B", "Button C", "Button D", "Button E", "Button F") fun main() = application {
Column(Modifier.fillMaxSize(), Arrangement.spacedBy(5.dp)) { Window(
buttons.forEachIndexed { index, name -> onCloseRequest = ::exitApplication,
// wrap button in BoxWithTooltip title = "Tooltip Example",
BoxWithTooltip( state = rememberWindowState(width = 300.dp, height = 300.dp)
modifier = Modifier.padding(start = 40.dp), ) {
tooltip = { val buttons = listOf("Button A", "Button B", "Button C", "Button D", "Button E", "Button F")
// composable tooltip content Column(Modifier.fillMaxSize(), Arrangement.spacedBy(5.dp)) {
Surface( buttons.forEachIndexed { index, name ->
modifier = Modifier.shadow(4.dp), // wrap button in BoxWithTooltip
color = Color(255, 255, 210), BoxWithTooltip(
shape = RoundedCornerShape(4.dp) modifier = Modifier.padding(start = 40.dp),
) { tooltip = {
Text( // composable tooltip content
text = "Tooltip for ${name}", Surface(
modifier = Modifier.padding(10.dp) modifier = Modifier.shadow(4.dp),
) color = Color(255, 255, 210),
} shape = RoundedCornerShape(4.dp)
}, ) {
delay = 600, // in milliseconds Text(
tooltipPlacement = TooltipPlacement( text = "Tooltip for ${name}",
anchor = TooltipPlacement.Anchor.Pointer, modifier = Modifier.padding(10.dp)
alignment = Alignment.BottomEnd, )
offset = if (index % 2 == 0) DpOffset(-16.dp, 0.dp) else DpOffset.Zero // tooltip offset }
) },
) { delay = 600, // in milliseconds
Button(onClick = {}) { Text(text = name) } tooltipPlacement = TooltipPlacement(
anchor = TooltipPlacement.Anchor.Pointer,
alignment = Alignment.BottomEnd,
offset = if (index % 2 == 0) DpOffset(-16.dp, 0.dp) else DpOffset.Zero // tooltip offset
)
) {
Button(onClick = {}) { Text(text = name) }
}
} }
} }
} }

49
tutorials/Getting_Started/README.md

@ -32,11 +32,11 @@ packaging JDK 15 or later must be used.
### Update the wizard plugin ### 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 { plugins {
kotlin("jvm") version "1.5.21" 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 { plugins {
kotlin("jvm") version "1.5.21" 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 { repositories {
@ -92,7 +92,6 @@ compose.desktop {
``` ```
Then create `src/main/kotlin/main.kt` and put the following code in there: Then create `src/main/kotlin/main.kt` and put the following code in there:
```kotlin ```kotlin
import androidx.compose.desktop.Window
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
@ -102,25 +101,35 @@ import androidx.compose.material.Text
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Window
fun main() = Window(title = "Compose for Desktop", size = IntSize(300, 300)) { import androidx.compose.ui.window.application
val count = remember { mutableStateOf(0) } import androidx.compose.ui.window.rememberWindowState
MaterialTheme {
Column(Modifier.fillMaxSize(), Arrangement.spacedBy(5.dp)) { @OptIn(ExperimentalComposeUiApi::class)
Button(modifier = Modifier.align(Alignment.CenterHorizontally), fun main() = application {
onClick = { Window(
count.value++ onCloseRequest = ::exitApplication,
}) { title = "Compose for Desktop",
Text(if (count.value == 0) "Hello World" else "Clicked ${count.value}!") state = rememberWindowState(width = 300.dp, height = 300.dp)
} ) {
Button(modifier = Modifier.align(Alignment.CenterHorizontally), val count = remember { mutableStateOf(0) }
onClick = { MaterialTheme {
count.value = 0 Column(Modifier.fillMaxSize(), Arrangement.spacedBy(5.dp)) {
Button(modifier = Modifier.align(Alignment.CenterHorizontally),
onClick = {
count.value++
}) {
Text(if (count.value == 0) "Hello World" else "Clicked ${count.value}!")
}
Button(modifier = Modifier.align(Alignment.CenterHorizontally),
onClick = {
count.value = 0
}) { }) {
Text("Reset") Text("Reset")
}
} }
} }
} }

439
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 ## 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) ![Sample](sample.png)
```kotlin ```kotlin
import androidx.compose.desktop.Window
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier 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)
Image( fun main() = singleWindowApplication {
bitmap = imageResource("images/sample.png"), // ImageBitmap Image(
contentDescription = "Sample", painter = painterResource("sample.png"), // ImageBitmap
modifier = Modifier.fillMaxSize() 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) ![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 ```kotlin
import androidx.compose.desktop.Window
import androidx.compose.foundation.Image 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.runtime.remember
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.graphics.painter.BitmapPainter
import org.jetbrains.skija.Image 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 import java.io.File
import java.io.IOException
fun main() {
Window { @OptIn(ExperimentalComposeUiApi::class)
val image = remember { imageFromFile(File("sample.png")) } fun main() = singleWindowApplication {
Image( val density = LocalDensity.current
bitmap = image, Column {
AsyncImage(
load = { loadImageBitmap(File("sample.png")) },
painterFor = { remember { BitmapPainter(it) } },
contentDescription = "Sample", 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 { @Composable
return Image.makeFromEncoded(file.readBytes()).asImageBitmap() 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 ## Drawing raw image data using native canvas
You may want to draw raw image data, in which case you can use `Canvas` and` drawIntoCanvas`. You may want to draw raw image data, in which case you can use `Canvas` and` drawIntoCanvas`.
```kotlin ```kotlin
import androidx.compose.desktop.Window
import androidx.compose.foundation.Canvas import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier 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.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.Bitmap
import org.jetbrains.skija.ColorAlphaType import org.jetbrains.skija.ColorAlphaType
import org.jetbrains.skija.IRect
import org.jetbrains.skija.ImageInfo 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() { private val sample = useResource("sample.png", ::loadImageBitmap)
Window {
val bitmap = remember { bitmapFromByteArray() } @OptIn(ExperimentalComposeUiApi::class)
Canvas( fun main() = singleWindowApplication {
modifier = Modifier.fillMaxSize() val bitmap = remember { bitmapFromByteArray(sample.getBytes(), sample.width, sample.height) }
) { Canvas(
drawIntoCanvas { canvas -> modifier = Modifier.fillMaxSize()
canvas.nativeCanvas.drawImageRect( ) {
Image.makeFromBitmap(bitmap), drawIntoCanvas { canvas ->
IRect(0, 0, bitmap.getWidth(), bitmap.getHeight()).toRect() canvas.drawImage(bitmap, Offset.Zero, Paint())
)
}
} }
} }
} }
fun bitmapFromByteArray(): Bitmap { fun bitmapFromByteArray(pixels: ByteArray, width: Int, height: Int): ImageBitmap {
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
val bitmap = Bitmap() val bitmap = Bitmap()
bitmap.allocPixels(ImageInfo.makeS32(image.width, image.height, ColorAlphaType.PREMUL)) bitmap.allocPixels(ImageInfo.makeS32(width, height, ColorAlphaType.PREMUL))
bitmap.installPixels(bitmap.getImageInfo(), pixels, (image.width * 4).toLong()) bitmap.installPixels(bitmap.imageInfo, pixels, (width * 4).toLong())
return bitmap.asImageBitmap()
return bitmap
} }
// creating byte array from BufferedImage // creating byte array from BufferedImage
private fun getBytes(image: BufferedImage): ByteArray { private fun ImageBitmap.getBytes(): ByteArray {
val width = image.width
val height = image.height
val buffer = IntArray(width * height) val buffer = IntArray(width * height)
image.getRGB(0, 0, width, height, buffer, 0, width) readPixels(buffer)
val pixels = ByteArray(width * height * 4) val pixels = ByteArray(width * height * 4)
@ -150,111 +208,27 @@ private fun getBytes(image: BufferedImage): ByteArray {
## Setting the application window icon ## Setting the application window icon
You have 2 ways to set icon for window: You can set the icon for the window via parameter in the `Window` function.
1. Via parameter in `Window` function (or in `AppWindow` constructor)
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 ```kotlin
import androidx.compose.desktop.Window import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.draw.paint
import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.res.painterResource
import org.jetbrains.skija.Image import androidx.compose.ui.window.Window
import java.awt.image.BufferedImage import androidx.compose.ui.window.application
import java.io.ByteArrayOutputStream
import java.io.File
import javax.imageio.ImageIO
fun main() { fun main() = application {
val image = getWindowIcon() val icon = painterResource("sample.png")
Window( Window(
icon = image onCloseRequest = ::exitApplication,
icon = icon
) { ) {
val imageAsset = remember { asImageAsset(image) } Box(Modifier.paint(icon).fillMaxSize())
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
}
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: You can create a tray icon for your application:
```kotlin ```kotlin
import androidx.compose.desktop.AppManager
import androidx.compose.desktop.Window
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.DisposableEffect
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.res.painterResource
import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.window.Tray
import androidx.compose.ui.window.v1.MenuItem import androidx.compose.ui.window.Window
import androidx.compose.ui.window.v1.Tray import androidx.compose.ui.window.application
import org.jetbrains.skija.Image
import java.awt.image.BufferedImage fun main() = application {
import java.io.ByteArrayOutputStream val icon = painterResource("sample.png")
import java.io.File
import javax.imageio.ImageIO Tray(
icon = icon,
fun main() { menu = {
val image = getWindowIcon() Item("Quit App", onClick = ::exitApplication)
Window {
DisposableEffect(Unit) {
val tray = Tray().apply {
icon(getWindowIcon())
menu(
MenuItem(
name = "Quit App",
onClick = { AppManager.exit() }
)
)
}
onDispose {
tray.remove()
}
} }
)
val imageAsset = asImageAsset(image) Window(onCloseRequest = ::exitApplication, icon = icon) {
Image( Image(
bitmap = imageAsset, painter = icon,
contentDescription = "Icon", contentDescription = "Icon",
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
) )
} }
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
}
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()
}
```
![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 {
Image(
imageVector = vectorXmlResource("images/compose-logo.xml"),
contentDescription = "Compose logo",
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

103
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: 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 ```kotlin
import androidx.compose.desktop.Window
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.Text
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.TextField import androidx.compose.material.TextField
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember 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.runtime.setValue
import androidx.compose.ui.ExperimentalComposeUiApi 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) @OptIn(ExperimentalComposeUiApi::class)
fun main() = Window(title = "Compose for Desktop", size = IntSize(300, 300)) { fun main() = singleWindowApplication {
MaterialTheme { MaterialTheme {
var consumedText by remember { mutableStateOf(0) } var consumedText by remember { mutableStateOf(0) }
var text by remember { mutableStateOf("") } var text by remember { mutableStateOf("") }
@ -51,12 +53,12 @@ fun main() = Window(title = "Compose for Desktop", size = IntSize(300, 300)) {
onValueChange = { text = it }, onValueChange = { text = it },
modifier = Modifier.onPreviewKeyEvent { modifier = Modifier.onPreviewKeyEvent {
when { when {
(it.isMetaPressed && it.key == Key.Minus) -> { (it.isCtrlPressed && it.key == Key.Minus) -> {
consumedText -= text.length consumedText -= text.length
text = "" text = ""
true true
} }
(it.isMetaPressed && it.key == Key.Equals) -> { (it.isCtrlPressed && it.key == Key.Equals) -> {
consumedText += text.length consumedText += text.length
text = "" text = ""
true true
@ -70,19 +72,15 @@ fun main() = Window(title = "Compose for Desktop", size = IntSize(300, 300)) {
} }
``` ```
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.
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.
![keyInputFilter](keyInputFilter.gif) ![keyInputFilter](keyInputFilter.gif)
## Window-scoped events ## 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 ``` 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.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
@ -91,32 +89,41 @@ import androidx.compose.material.Button
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.input.key.Key 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.isShiftPressed
import androidx.compose.ui.input.key.key 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.ui.unit.dp
import androidx.compose.runtime.getValue import androidx.compose.ui.window.Dialog
import androidx.compose.runtime.setValue import androidx.compose.ui.window.singleWindowApplication
private var cleared by mutableStateOf(false)
@OptIn(ExperimentalComposeUiApi::class) @OptIn(ExperimentalComposeUiApi::class)
fun main() = Window(title = "Compose for Desktop", size = IntSize(300, 300)) { fun main() = singleWindowApplication(
MaterialTheme { onKeyEvent = {
var cleared by remember { mutableStateOf(false) } if (
LocalAppWindow.current.keyboard.onKeyEvent = { it.isCtrlPressed &&
if (it.isMetaPressed && it.isShiftPressed && it.key == Key.C) { it.isShiftPressed &&
cleared = true it.key == Key.C &&
true it.type == KeyEventType.KeyDown
} else { ) {
false cleared = true
} true
} else {
false
} }
}
) {
MaterialTheme {
if (cleared) { if (cleared) {
Text("The App was cleared!") Text("The App was cleared!")
} else { } else {
@ -128,25 +135,29 @@ fun main() = Window(title = "Compose for Desktop", size = IntSize(300, 300)) {
@OptIn(ExperimentalComposeUiApi::class) @OptIn(ExperimentalComposeUiApi::class)
@Composable @Composable
fun App() { fun App() {
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
}
}) {
Text("I'm dialog!")
}
}
Column(Modifier.fillMaxSize(), Arrangement.spacedBy(5.dp)) { Column(Modifier.fillMaxSize(), Arrangement.spacedBy(5.dp)) {
Button( Button(
modifier = Modifier.padding(4.dp), modifier = Modifier.padding(4.dp),
onClick = { onClick = { isDialogOpen = true }
AppWindow(size = IntSize(200, 200)).also { window ->
window.keyboard.onPreviewKeyEvent = {
if (it.key == Key.Escape) {
window.close()
true
} else {
false
}
}
}.show {
Text("I'm popup!")
}
}
) { ) {
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: so code like this will work on both platforms:
```kotlin ```kotlin
import androidx.compose.desktop.Window
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable 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.material.Text
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.sp 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) } var count by remember { mutableStateOf(0) }
Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxWidth()) { Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxWidth()) {
var text by remember { mutableStateOf("Click magenta box!") } 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 Let's create a window and install a pointer move filter on it that changes the background
color according to the mouse pointer position: color according to the mouse pointer position:
```kotlin ```kotlin
import androidx.compose.desktop.Window
import androidx.compose.foundation.background 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.getValue
import androidx.compose.runtime.setValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerMoveFilter 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)) } var color by remember { mutableStateOf(Color(0, 0, 0)) }
Box( Box(
modifier = Modifier 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: Compose for Desktop also supports pointer enter and exit handlers, like this:
```kotlin ```kotlin
import androidx.compose.desktop.Window
import androidx.compose.foundation.background 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.material.Text
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerMoveFilter import androidx.compose.ui.input.pointer.pointerMoveFilter
import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp 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( Column(
Modifier.background(Color.White), Modifier.background(Color.White),
verticalArrangement = Arrangement.spacedBy(10.dp) 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. 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 ```kotlin
import androidx.compose.desktop.Window
import androidx.compose.foundation.ExperimentalDesktopApi import androidx.compose.foundation.ExperimentalDesktopApi
import androidx.compose.foundation.mouseClickable import androidx.compose.foundation.mouseClickable
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.IntSize import androidx.compose.ui.window.singleWindowApplication
@OptIn(ExperimentalDesktopApi::class) @OptIn(ExperimentalComposeUiApi::class, ExperimentalDesktopApi::class)
fun main() = Window(title = "Compose for Desktop", size = IntSize(400, 400)) { fun main() = singleWindowApplication {
var clickableText by remember { mutableStateOf("Click me!") } var clickableText by remember { mutableStateOf("Click me!") }
Text( Text(

20
tutorials/Native_distributions_and_local_execution/README.md

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

18
tutorials/Navigation/README.md

@ -289,21 +289,23 @@ Application and Root initialisation:
``` kotlin ``` kotlin
import androidx.compose.desktop.DesktopTheme import androidx.compose.desktop.DesktopTheme
import androidx.compose.desktop.Window
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface import androidx.compose.material.Surface
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.window.singleWindowApplication
import com.arkivanov.decompose.extensions.compose.jetbrains.rememberRootComponent import com.arkivanov.decompose.extensions.compose.jetbrains.rememberRootComponent
fun main() { @OptIn(ExperimentalComposeUiApi::class)
Window("Navigation tutorial") { fun main() = singleWindowApplication(
Surface(modifier = Modifier.fillMaxSize()) { title = "Navigation tutorial"
MaterialTheme { ) {
DesktopTheme { Surface(modifier = Modifier.fillMaxSize()) {
RootUi(root()) // Render the Root and its children MaterialTheme {
} DesktopTheme {
RootUi(root()) // Render the Root and its children
} }
} }
} }

138
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`. 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 ```kotlin
import androidx.compose.desktop.AppManager
import androidx.compose.desktop.ComposePanel
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column 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.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.material.Text
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Button import androidx.compose.material.Button
import androidx.compose.material.Surface import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.awt.ComposePanel
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import java.awt.BorderLayout import java.awt.BorderLayout
import java.awt.Dimension 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.JButton
import javax.swing.JFrame
import javax.swing.SwingUtilities import javax.swing.SwingUtilities
import javax.swing.WindowConstants import javax.swing.WindowConstants
@ -44,17 +41,7 @@ val northClicks = mutableStateOf(0)
val westClicks = mutableStateOf(0) val westClicks = mutableStateOf(0)
val eastClicks = mutableStateOf(0) val eastClicks = mutableStateOf(0)
fun main() { fun main() = SwingUtilities.invokeLater {
// explicitly clear the application events
AppManager.setEvents(
onAppStart = null,
onAppExit = null,
onWindowsEmpty = null
)
SwingComposeWindow()
}
fun SwingComposeWindow() = SwingUtilities.invokeLater {
val window = JFrame() val window = JFrame()
// creating ComposePanel // creating ComposePanel
@ -62,10 +49,9 @@ fun SwingComposeWindow() = SwingUtilities.invokeLater {
window.defaultCloseOperation = WindowConstants.EXIT_ON_CLOSE window.defaultCloseOperation = WindowConstants.EXIT_ON_CLOSE
window.title = "SwingComposeWindow" window.title = "SwingComposeWindow"
window.contentPane.add(actionButton("NORTH", action = { northClicks.value++ }), BorderLayout.NORTH)
window.contentPane.add(actionButton("NORTH", { northClicks.value++ }), BorderLayout.NORTH) window.contentPane.add(actionButton("WEST", action = { westClicks.value++ }), BorderLayout.WEST)
window.contentPane.add(actionButton("WEST", { westClicks.value++ }), BorderLayout.WEST) window.contentPane.add(actionButton("EAST", action = { eastClicks.value++ }), BorderLayout.EAST)
window.contentPane.add(actionButton("EAST", { eastClicks.value++ }), BorderLayout.EAST)
window.contentPane.add( window.contentPane.add(
actionButton( actionButton(
text = "SOUTH/REMOVE COMPOSE", text = "SOUTH/REMOVE COMPOSE",
@ -85,19 +71,14 @@ fun SwingComposeWindow() = SwingUtilities.invokeLater {
} }
window.setSize(800, 600) 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) val button = JButton(text)
button.setToolTipText("Tooltip for $text button.") button.toolTipText = "Tooltip for $text button."
button.setPreferredSize(Dimension(100, 100)) button.preferredSize = Dimension(100, 100)
button.addActionListener(object : ActionListener { button.addActionListener { action() }
public override fun actionPerformed(e: ActionEvent) {
action?.invoke()
}
})
return button return button
} }
@ -147,13 +128,11 @@ fun Counter(text: String, counter: MutableState<Int>) {
![IntegrationWithSwing](screenshot.png) ![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`. 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 ```kotlin
import androidx.compose.desktop.SwingPanel
import androidx.compose.desktop.Window
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
@ -168,57 +147,56 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.awt.SwingPanel
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.singleWindowApplication
import java.awt.Component 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.BoxLayout
import javax.swing.JButton import javax.swing.JButton
import javax.swing.JPanel import javax.swing.JPanel
fun main() { @OptIn(ExperimentalComposeUiApi::class)
Window { fun main() = singleWindowApplication {
val counter = remember { mutableStateOf(0) } val counter = remember { mutableStateOf(0) }
val inc: () -> Unit = { counter.value++ } val inc: () -> Unit = { counter.value++ }
val dec: () -> Unit = { counter.value-- } val dec: () -> Unit = { counter.value-- }
Box( Box(
modifier = Modifier.fillMaxWidth().height(60.dp).padding(top = 20.dp), modifier = Modifier.fillMaxWidth().height(60.dp).padding(top = 20.dp),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Text("Counter: ${counter.value}") Text("Counter: ${counter.value}")
} }
Box( Box(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) {
Column(
modifier = Modifier.padding(top = 80.dp, bottom = 20.dp)
) { ) {
Column( Button("1. Compose Button: increment", inc)
modifier = Modifier.padding(top = 80.dp, bottom = 20.dp) Spacer(modifier = Modifier.height(20.dp))
) {
Button("1. Compose Button: increment", inc) SwingPanel(
Spacer(modifier = Modifier.height(20.dp)) background = Color.White,
modifier = Modifier.size(270.dp, 90.dp),
SwingPanel( factory = {
background = Color.White, JPanel().apply {
modifier = Modifier.size(270.dp, 90.dp), layout = BoxLayout(this, BoxLayout.Y_AXIS)
factory = { add(actionButton("1. Swing Button: decrement", dec))
JPanel().apply { add(actionButton("2. Swing Button: decrement", dec))
setLayout(BoxLayout(this, BoxLayout.Y_AXIS)) add(actionButton("3. Swing Button: decrement", dec))
add(actionButton("1. Swing Button: decrement", dec))
add(actionButton("2. Swing Button: decrement", dec))
add(actionButton("3. Swing Button: decrement", dec))
}
} }
) }
)
Spacer(modifier = Modifier.height(20.dp)) Spacer(modifier = Modifier.height(20.dp))
Button("2. Compose Button: increment", inc) Button("2. Compose Button: increment", inc)
}
} }
} }
} }
@ -235,15 +213,11 @@ fun Button(text: String = "", action: (() -> Unit)? = null) {
fun actionButton( fun actionButton(
text: String, text: String,
action: (() -> Unit)? = null action: () -> Unit
): JButton { ): JButton {
val button = JButton(text) val button = JButton(text)
button.setAlignmentX(Component.CENTER_ALIGNMENT) button.alignmentX = Component.CENTER_ALIGNMENT
button.addActionListener(object : ActionListener { button.addActionListener { action() }
public override fun actionPerformed(e: ActionEvent) {
action?.invoke()
}
})
return button 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

137
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.Box
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier 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.Notification
import androidx.compose.ui.window.Tray import androidx.compose.ui.window.Tray
import androidx.compose.ui.window.Window import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application import androidx.compose.ui.window.application
import androidx.compose.ui.window.rememberTrayState import androidx.compose.ui.window.rememberTrayState
import java.awt.Color
import java.awt.image.BufferedImage
@OptIn(ExperimentalComposeUiApi::class)
fun main() = application { fun main() = application {
var count by remember { mutableStateOf(0) } var count by remember { mutableStateOf(0) }
var isOpen by remember { mutableStateOf(true) } var isOpen by remember { mutableStateOf(true) }
@ -39,13 +40,13 @@ fun main() = application {
if (isOpen) { if (isOpen) {
Window( Window(
onCloseRequest = ::exitApplication, onCloseRequest = ::exitApplication,
icon = remember { getMyAppIcon() } icon = MyAppIcon
) { ) {
val trayState = rememberTrayState() val trayState = rememberTrayState()
val notification = Notification("Notification", "Message from MyApp!") val notification = Notification("Notification", "Message from MyApp!")
Tray( Tray(
state = trayState, state = trayState,
icon = remember { getTrayIcon() }, icon = TrayIcon,
menu = { menu = {
Item( Item(
"Increment value", "Increment value",
@ -73,34 +74,28 @@ fun main() = application {
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Text(text = "Value: ${count}") Text(text = "Value: $count")
} }
} }
} }
} }
fun getMyAppIcon(): BufferedImage { object MyAppIcon : Painter() {
val size = 256 override val intrinsicSize = Size(256f, 256f)
val image = BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB)
val graphics = image.createGraphics() override fun DrawScope.onDraw() {
graphics.color = Color.green drawOval(Color.Green, Offset(size.width / 4, 0f), Size(size.width / 2f, size.height))
graphics.fillOval(size / 4, 0, size / 2, size) drawOval(Color.Blue, Offset(0f, size.height / 4), Size(size.width, size.height / 2f))
graphics.color = Color.blue drawOval(Color.Red, Offset(size.width / 4, size.height / 4), Size(size.width / 2f, size.height / 2f))
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
} }
fun getTrayIcon(): BufferedImage { object TrayIcon : Painter() {
val size = 256 override val intrinsicSize = Size(256f, 256f)
val image = BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB)
val graphics = image.createGraphics() override fun DrawScope.onDraw() {
graphics.color = Color.orange drawOval(Color(0xFFFFA500))
graphics.fillOval(0, 0, size, size) }
graphics.dispose()
return image
} }
``` ```
@ -121,57 +116,67 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier 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.MenuBar
import androidx.compose.ui.window.Window import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application import androidx.compose.ui.window.application
@OptIn(ExperimentalComposeUiApi::class) @OptIn(ExperimentalComposeUiApi::class)
fun main() { fun main() = application {
// 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 var action by remember { mutableStateOf("Last action: None") }
System.setProperty("skiko.rendering.laf.global", "true") var isOpen by remember { mutableStateOf(true) }
application { if (isOpen) {
var action by remember { mutableStateOf("Last action: None") } var isSubmenuShowing by remember { mutableStateOf(false) }
var isOpen by remember { mutableStateOf(true) }
Window(onCloseRequest = { isOpen = false }) {
if (isOpen) { MenuBar {
var isSubmenuShowing by remember { mutableStateOf(false) } Menu("File", mnemonic = 'F') {
Item("Copy", onClick = { action = "Last action: Copy" }, shortcut = KeyShortcut(Key.C, ctrl = true))
Window(onCloseRequest = { isOpen = false }) { Item("Paste", onClick = { action = "Last action: Paste" }, shortcut = KeyShortcut(Key.V, ctrl = true))
MenuBar { }
Menu("Actions") { Menu("Actions", mnemonic = 'A') {
Item( CheckboxItem(
if (isSubmenuShowing) "Hide advanced settings" else "Show advanced settings", "Advanced settings",
onClick = { checked = isSubmenuShowing,
isSubmenuShowing = !isSubmenuShowing onCheckedChange = {
} isSubmenuShowing = !isSubmenuShowing
) }
if (isSubmenuShowing) { )
Menu("Settings") { if (isSubmenuShowing) {
Item("Setting 1", onClick = { action = "Last action: Setting 1" }) Menu("Settings") {
Item("Setting 2", onClick = { action = "Last action: Setting 2" }) Item("Setting 1", onClick = { action = "Last action: Setting 1" })
} Item("Setting 2", onClick = { action = "Last action: Setting 2" })
} }
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" },)
} }
Separator()
Item("About", icon = TrayIcon, onClick = { action = "Last action: About" })
Item("Exit", onClick = { isOpen = false }, shortcut = KeyShortcut(Key.Escape), mnemonic = 'E')
} }
}
Box( Box(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Text(text = action) Text(text = action)
}
} }
} }
} }
} }
object TrayIcon : Painter() {
override val intrinsicSize = Size(256f, 256f)
override fun DrawScope.onDraw() {
drawOval(Color(0xFFFFA500))
}
}
``` ```
![](window_menubar.gif) ![](window_menubar.gif)

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

96
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: 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 ```kotlin
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.window.Window import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application import androidx.compose.ui.window.application
@OptIn(ExperimentalComposeUiApi::class)
fun main() = application { fun main() = application {
Window(onCloseRequest = ::exitApplication) { Window(onCloseRequest = ::exitApplication) {
// Content // Content
@ -33,11 +31,9 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.window.Window import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application import androidx.compose.ui.window.application
@OptIn(ExperimentalComposeUiApi::class)
fun main() = application { fun main() = application {
var fileName by remember { mutableStateOf("Untitled") } var fileName by remember { mutableStateOf("Untitled") }
@ -61,18 +57,18 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.window.Window import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application import androidx.compose.ui.window.application
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@OptIn(ExperimentalComposeUiApi::class)
fun main() = application { fun main() = application {
var isPerformingTask by remember { mutableStateOf(true) } var isPerformingTask by remember { mutableStateOf(true) }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
delay(2000) // Do some heavy lifting delay(2000) // Do some heavy lifting
isPerformingTask = false isPerformingTask = false
} }
if (isPerformingTask) { if (isPerformingTask) {
Window(onCloseRequest = ::exitApplication) { Window(onCloseRequest = ::exitApplication) {
Text("Performing some tasks. Please wait!") Text("Performing some tasks. Please wait!")
@ -96,16 +92,14 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.Window import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application import androidx.compose.ui.window.application
@OptIn(ExperimentalComposeUiApi::class)
fun main() = application { fun main() = application {
var isOpen by remember { mutableStateOf(true) } var isOpen by remember { mutableStateOf(true) }
var isAskingToClose by remember { mutableStateOf(false) } var isAskingToClose by remember { mutableStateOf(false) }
if (isOpen) { if (isOpen) {
Window( Window(
onCloseRequest = { isAskingToClose = true } onCloseRequest = { isAskingToClose = true }
@ -136,16 +130,15 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue 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.Tray
import androidx.compose.ui.window.Window import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application import androidx.compose.ui.window.application
import androidx.compose.ui.window.rememberWindowState
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import java.awt.Color
import java.awt.image.BufferedImage
@OptIn(ExperimentalComposeUiApi::class)
fun main() = application { fun main() = application {
var isVisible by remember { mutableStateOf(true) } var isVisible by remember { mutableStateOf(true) }
@ -166,7 +159,7 @@ fun main() = application {
if (!isVisible) { if (!isVisible) {
Tray( Tray(
remember { getTrayIcon() }, TrayIcon,
hint = "Counter", hint = "Counter",
onAction = { isVisible = true }, onAction = { isVisible = true },
menu = { menu = {
@ -176,14 +169,12 @@ fun main() = application {
} }
} }
fun getTrayIcon(): BufferedImage { object TrayIcon : Painter() {
val size = 256 override val intrinsicSize = Size(256f, 256f)
val image = BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB)
val graphics = image.createGraphics() override fun DrawScope.onDraw() {
graphics.color = Color.orange drawOval(Color(0xFFFFA500))
graphics.fillOval(0, 0, size, size) }
graphics.dispose()
return image
} }
``` ```
![](hide_instead_of_close.gif) ![](hide_instead_of_close.gif)
@ -195,11 +186,11 @@ import androidx.compose.runtime.key
import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.window.ApplicationScope
import androidx.compose.ui.window.MenuBar import androidx.compose.ui.window.MenuBar
import androidx.compose.ui.window.Window import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application import androidx.compose.ui.window.application
@OptIn(ExperimentalComposeUiApi::class)
fun main() = application { fun main() = application {
val applicationState = remember { MyApplicationState() } val applicationState = remember { MyApplicationState() }
@ -212,7 +203,7 @@ fun main() = application {
@OptIn(ExperimentalComposeUiApi::class) @OptIn(ExperimentalComposeUiApi::class)
@Composable @Composable
private fun MyWindow( private fun ApplicationScope.MyWindow(
state: MyWindowState state: MyWindowState
) = Window(onCloseRequest = state::close, title = state.title) { ) = Window(onCloseRequest = state::close, title = state.title) {
MenuBar { MenuBar {
@ -271,7 +262,6 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.material.Checkbox import androidx.compose.material.Checkbox
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Window 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.application
import androidx.compose.ui.window.rememberWindowState import androidx.compose.ui.window.rememberWindowState
@OptIn(ExperimentalComposeUiApi::class)
fun main() = application { fun main() = application {
val state = rememberWindowState(placement = WindowPlacement.Maximized) val state = rememberWindowState(placement = WindowPlacement.Maximized)
@ -344,10 +333,9 @@ fun main() = application {
## Listening the state of the window ## 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): 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.LaunchedEffect
import androidx.compose.runtime.snapshotFlow import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.window.Window import androidx.compose.ui.window.Window
import androidx.compose.ui.window.WindowPosition import androidx.compose.ui.window.WindowPosition
import androidx.compose.ui.window.WindowSize import androidx.compose.ui.window.WindowSize
@ -357,20 +345,19 @@ import kotlinx.coroutines.flow.filterNot
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
@OptIn(ExperimentalComposeUiApi::class)
fun main() = application { fun main() = application {
val state = rememberWindowState() val state = rememberWindowState()
Window(state) { Window(onCloseRequest = ::exitApplication, state) {
// Content // Content
LaunchedEffect(state) { LaunchedEffect(state) {
snapshotFlow { state.size } snapshotFlow { state.size }
.onEach(::onWindowResize) .onEach(::onWindowResize)
.launchIn(this) .launchIn(this)
snapshotFlow { state.position } snapshotFlow { state.position }
.filterNot { it.isInitial } .filterNot { it.isSpecified }
.onEach(::onWindowRelocate) .onEach(::onWindowRelocate)
.launchIn(this) .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 ## Dialogs
There are two types of window – modal and regular. Below are the functions for creating each: 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.Button
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogState import androidx.compose.ui.window.DialogState
import androidx.compose.ui.window.Window import androidx.compose.ui.window.Window
import androidx.compose.ui.window.WindowPosition import androidx.compose.ui.window.WindowPosition
import androidx.compose.ui.window.WindowState
import androidx.compose.ui.window.application import androidx.compose.ui.window.application
@OptIn(ExperimentalComposeUiApi::class)
fun main() = application { fun main() = application {
Window( Window(
onCloseRequest = ::exitApplication, onCloseRequest = ::exitApplication,
@ -465,13 +420,11 @@ fun main() = application {
## Swing interoperability ## Swing interoperability
Because Compose for Desktop uses Swing under the hood, it is possible to create a window using Swing directly: Because Compose for Desktop uses Swing under the hood, it is possible to create a window using Swing directly:
```kotlin ```kotlin
import androidx.compose.desktop.ComposeWindow import androidx.compose.ui.awt.ComposeWindow
import androidx.compose.ui.ExperimentalComposeUiApi
import java.awt.Dimension import java.awt.Dimension
import javax.swing.JFrame import javax.swing.JFrame
import javax.swing.SwingUtilities import javax.swing.SwingUtilities
@OptIn(ExperimentalComposeUiApi::class)
fun main() = SwingUtilities.invokeLater { fun main() = SwingUtilities.invokeLater {
ComposeWindow().apply { ComposeWindow().apply {
size = Dimension(300, 300) size = Dimension(300, 300)
@ -509,13 +462,11 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.window.AwtWindow import androidx.compose.ui.window.AwtWindow
import androidx.compose.ui.window.application import androidx.compose.ui.window.application
import java.awt.FileDialog import java.awt.FileDialog
import java.awt.Frame import java.awt.Frame
@OptIn(ExperimentalComposeUiApi::class)
fun main() = application { fun main() = application {
var isOpen by remember { mutableStateOf(true) } var isOpen by remember { mutableStateOf(true) }
@ -529,7 +480,6 @@ fun main() = application {
} }
} }
@OptIn(ExperimentalComposeUiApi::class)
@Composable @Composable
private fun FileDialog( private fun FileDialog(
parent: Frame? = null, parent: Frame? = null,
@ -547,4 +497,4 @@ private fun FileDialog(
}, },
dispose = FileDialog::dispose dispose = FileDialog::dispose
) )
``` ```
Loading…
Cancel
Save