diff --git a/README.md b/README.md index 39fc46d231..10a30bf80d 100644 --- a/README.md +++ b/README.md @@ -35,10 +35,8 @@ Preview functionality (check your application UI without building/running it) fo * [Mouse events and hover](tutorials/Mouse_Events) * [Scrolling and scrollbars](tutorials/Desktop_Components) * [Tooltips](tutorials/Desktop_Components#tooltips) - * [Top level windows management](tutorials/Window_API) - * [Top level windows management (new Composable API, experimental)](tutorials/Window_API_new) - * [Menu, tray, notifications](tutorials/Tray_Notifications_MenuBar) - * [Menu, tray, notifications (new Composable API, experimental)](tutorials/Tray_Notifications_MenuBar_new) + * [Top level windows management](tutorials/Window_API_new) + * [Menu, tray, notifications](tutorials/Tray_Notifications_MenuBar_new) * [Keyboard support](tutorials/Keyboard) * [Building native distribution](tutorials/Native_distributions_and_local_execution) * [Signing and notarization](tutorials/Signing_and_notarization_on_macOS) diff --git a/tutorials/Image_And_Icons_Manipulations/compose-logo.xml b/artwork/compose-logo.xml similarity index 100% rename from tutorials/Image_And_Icons_Manipulations/compose-logo.xml rename to artwork/compose-logo.xml diff --git a/compose/frameworks/support b/compose/frameworks/support index 3de47bade6..883fe193e4 160000 --- a/compose/frameworks/support +++ b/compose/frameworks/support @@ -1 +1 @@ -Subproject commit 3de47bade63fae3a9ee0b7b5a5d9028ccaf0e3ef +Subproject commit 883fe193e46c2ad23b3aa50d4122f97a679b56ea diff --git a/templates/desktop-template/build.gradle.kts b/templates/desktop-template/build.gradle.kts index 9255c3f0da..e69a475ad5 100644 --- a/templates/desktop-template/build.gradle.kts +++ b/templates/desktop-template/build.gradle.kts @@ -5,7 +5,7 @@ plugins { // __KOTLIN_COMPOSE_VERSION__ kotlin("jvm") version "1.5.21" // __LATEST_COMPOSE_RELEASE_VERSION__ - id("org.jetbrains.compose") version (System.getenv("COMPOSE_TEMPLATE_COMPOSE_VERSION") ?: "0.5.0-build262") + id("org.jetbrains.compose") version (System.getenv("COMPOSE_TEMPLATE_COMPOSE_VERSION") ?: "0.5.0-build270") } repositories { diff --git a/templates/desktop-template/src/main/kotlin/main.kt b/templates/desktop-template/src/main/kotlin/main.kt index 85d8b04296..3c1383e436 100644 --- a/templates/desktop-template/src/main/kotlin/main.kt +++ b/templates/desktop-template/src/main/kotlin/main.kt @@ -1,4 +1,3 @@ -import androidx.compose.desktop.Window import androidx.compose.material.Text import androidx.compose.material.Button import androidx.compose.material.MaterialTheme @@ -6,15 +5,19 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.ui.window.Window +import androidx.compose.ui.window.application -fun main() = Window { - var text by remember { mutableStateOf("Hello, World!") } +fun main() = application { + Window(onCloseRequest = ::exitApplication) { + var text by remember { mutableStateOf("Hello, World!") } - MaterialTheme { - Button(onClick = { - text = "Hello, Desktop!" - }) { - Text(text) + MaterialTheme { + Button(onClick = { + text = "Hello, Desktop!" + }) { + Text(text) + } } } } diff --git a/templates/multiplatform-template/build.gradle.kts b/templates/multiplatform-template/build.gradle.kts index 7114839f1c..74bd14a6ef 100644 --- a/templates/multiplatform-template/build.gradle.kts +++ b/templates/multiplatform-template/build.gradle.kts @@ -1,6 +1,6 @@ buildscript { // __LATEST_COMPOSE_RELEASE_VERSION__ - val composeVersion = System.getenv("COMPOSE_TEMPLATE_COMPOSE_VERSION") ?: "0.5.0-build262" + val composeVersion = System.getenv("COMPOSE_TEMPLATE_COMPOSE_VERSION") ?: "0.5.0-build270" repositories { google() diff --git a/templates/multiplatform-template/desktop/src/jvmMain/kotlin/main.kt b/templates/multiplatform-template/desktop/src/jvmMain/kotlin/main.kt index c810ea3d8b..2543564b01 100644 --- a/templates/multiplatform-template/desktop/src/jvmMain/kotlin/main.kt +++ b/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 { - App() +fun main() = application { + Window(onCloseRequest = ::exitApplication) { + App() + } } \ No newline at end of file diff --git a/tutorials/Desktop_Components/README.md b/tutorials/Desktop_Components/README.md index dfa894b96a..5ace8770fc 100644 --- a/tutorials/Desktop_Components/README.md +++ b/tutorials/Desktop_Components/README.md @@ -9,7 +9,6 @@ In this tutorial, we will show you how to use desktop-specific components of Com You can apply scrollbars to scrollable components. The scrollbar and scrollable components share a common state to synchronize with each other. For example, `VerticalScrollbar` can be attached to `Modifier.verticalScroll`, and `LazyColumnFor` and `HorizontalScrollbar` can be attached to `Modifier.horizontalScroll` and `LazyRowFor`. ```kotlin -import androidx.compose.desktop.Window import androidx.compose.foundation.HorizontalScrollbar import androidx.compose.foundation.VerticalScrollbar import androidx.compose.foundation.background @@ -29,49 +28,53 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.WindowState +import androidx.compose.ui.window.singleWindowApplication + +@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( - modifier = Modifier.fillMaxSize() - .background(color = Color(180, 180, 180)) - .padding(10.dp) + modifier = Modifier + .fillMaxSize() + .verticalScroll(stateVertical) + .padding(end = 12.dp, bottom = 12.dp) + .horizontalScroll(stateHorizontal) ) { - val stateVertical = rememberScrollState(0) - val stateHorizontal = rememberScrollState(0) - - Box( - modifier = Modifier - .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)) - } + 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`. ```kotlin -import androidx.compose.desktop.Window import androidx.compose.foundation.VerticalScrollbar import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box @@ -112,13 +114,21 @@ import androidx.compose.foundation.rememberScrollbarAdapter import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Window +import androidx.compose.ui.window.application +import androidx.compose.ui.window.rememberWindowState -fun main() { - Window(title = "Scrollbars", size = IntSize(250, 400)) { +@OptIn(ExperimentalComposeUiApi::class) +fun main() = application { + Window( + onCloseRequest = ::exitApplication, + title = "Scrollbars", + state = rememberWindowState(width = 250.dp, height = 400.dp) + ) { LazyScrollable() } } @@ -170,7 +180,6 @@ Scrollbars support themes to change their appearance. The example below shows ho ```kotlin import androidx.compose.desktop.DesktopTheme -import androidx.compose.desktop.Window import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxHeight @@ -188,13 +197,21 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.graphics.Color import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.window.Window +import androidx.compose.ui.window.application +import androidx.compose.ui.window.rememberWindowState -fun main() { - Window(title = "Scrollbars", size = IntSize(280, 400)) { +@OptIn(ExperimentalComposeUiApi::class) +fun main() = application { + Window( + onCloseRequest = ::exitApplication, + title = "Scrollbars", + state = rememberWindowState(width = 250.dp, height = 400.dp) + ) { MaterialTheme { DesktopTheme { Box( @@ -250,11 +267,10 @@ fun TextBox(text: String = "Item") { You can add tooltip to any components using `BoxWithTooltip`. Basically `BoxWithTooltip` is a `Box` with the ability to show a tooltip, and has the same arguments and behavior as `Box`. The main arguments of the `BoxWithTooltip` function: - tooltip - composable content representing tooltip + - tooltipPlacement - describes how to place tooltip. You can specify an anchor (the mouse cursor or the component), an offset and an alignment - delay - time delay in milliseconds after which the tooltip will be shown (default is 500 ms) - - offset - tooltip offset, the default position of the tooltip is under the mouse cursor ```kotlin -import androidx.compose.desktop.Window import androidx.compose.foundation.BoxWithTooltip import androidx.compose.foundation.TooltipPlacement import androidx.compose.foundation.layout.Arrangement @@ -266,41 +282,51 @@ import androidx.compose.material.Button import androidx.compose.material.Surface import androidx.compose.material.Text import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier import androidx.compose.ui.draw.shadow import androidx.compose.ui.graphics.Color -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.DpOffset -import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Window +import androidx.compose.ui.window.application +import androidx.compose.ui.window.rememberWindowState -fun main() = Window(title = "Tooltip Example", size = IntSize(300, 300)) { - val buttons = listOf("Button A", "Button B", "Button C", "Button D", "Button E", "Button F") - Column(Modifier.fillMaxSize(), Arrangement.spacedBy(5.dp)) { - buttons.forEachIndexed { index, name -> - // wrap button in BoxWithTooltip - BoxWithTooltip( - modifier = Modifier.padding(start = 40.dp), - tooltip = { - // composable tooltip content - Surface( - modifier = Modifier.shadow(4.dp), - color = Color(255, 255, 210), - shape = RoundedCornerShape(4.dp) - ) { - Text( - text = "Tooltip for ${name}", - modifier = Modifier.padding(10.dp) - ) - } - }, - delay = 600, // in milliseconds - 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) } +@OptIn(ExperimentalComposeUiApi::class) +fun main() = application { + Window( + onCloseRequest = ::exitApplication, + title = "Tooltip Example", + state = rememberWindowState(width = 300.dp, height = 300.dp) + ) { + val buttons = listOf("Button A", "Button B", "Button C", "Button D", "Button E", "Button F") + Column(Modifier.fillMaxSize(), Arrangement.spacedBy(5.dp)) { + buttons.forEachIndexed { index, name -> + // wrap button in BoxWithTooltip + BoxWithTooltip( + modifier = Modifier.padding(start = 40.dp), + tooltip = { + // composable tooltip content + Surface( + modifier = Modifier.shadow(4.dp), + color = Color(255, 255, 210), + shape = RoundedCornerShape(4.dp) + ) { + Text( + text = "Tooltip for ${name}", + modifier = Modifier.padding(10.dp) + ) + } + }, + delay = 600, // in milliseconds + 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) } + } } } } diff --git a/tutorials/Getting_Started/README.md b/tutorials/Getting_Started/README.md index d3af299108..201f067c86 100644 --- a/tutorials/Getting_Started/README.md +++ b/tutorials/Getting_Started/README.md @@ -32,11 +32,11 @@ packaging JDK 15 or later must be used. ### Update the wizard plugin -The Сompose plugin version used in the wizard above may be not the last. Update the version of the plugin to the latest available by editing the `build.gradle.kts` file, finding and updating the version information as shown below. In this example the latest version of the plugin was 0.5.0-build262 and a compatible version of kotlin was 1.5.21. For the latest versions, see the [latest versions](https://github.com/JetBrains/compose-jb/releases) site and the [Kotlin](https://kotlinlang.org/) site. +The Сompose plugin version used in the wizard above may be not the last. Update the version of the plugin to the latest available by editing the `build.gradle.kts` file, finding and updating the version information as shown below. In this example the latest version of the plugin was 0.5.0-build270 and a compatible version of kotlin was 1.5.21. For the latest versions, see the [latest versions](https://github.com/JetBrains/compose-jb/releases) site and the [Kotlin](https://kotlinlang.org/) site. ``` plugins { kotlin("jvm") version "1.5.21" - id("org.jetbrains.compose") version "0.5.0-build262" + id("org.jetbrains.compose") version "0.5.0-build270" } ``` @@ -72,7 +72,7 @@ import org.jetbrains.compose.compose plugins { kotlin("jvm") version "1.5.21" - id("org.jetbrains.compose") version "0.5.0-build262" + id("org.jetbrains.compose") version "0.5.0-build270" } repositories { @@ -92,7 +92,6 @@ compose.desktop { ``` Then create `src/main/kotlin/main.kt` and put the following code in there: ```kotlin -import androidx.compose.desktop.Window import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize @@ -102,25 +101,35 @@ import androidx.compose.material.Text import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp - -fun main() = Window(title = "Compose for Desktop", size = IntSize(300, 300)) { - val count = remember { mutableStateOf(0) } - MaterialTheme { - 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 +import androidx.compose.ui.window.Window +import androidx.compose.ui.window.application +import androidx.compose.ui.window.rememberWindowState + +@OptIn(ExperimentalComposeUiApi::class) +fun main() = application { + Window( + onCloseRequest = ::exitApplication, + title = "Compose for Desktop", + state = rememberWindowState(width = 300.dp, height = 300.dp) + ) { + val count = remember { mutableStateOf(0) } + MaterialTheme { + Column(Modifier.fillMaxSize(), Arrangement.spacedBy(5.dp)) { + 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") + } } } } diff --git a/tutorials/Image_And_Icons_Manipulations/README.md b/tutorials/Image_And_Icons_Manipulations/README.md index ad88fd293b..c2e242fad6 100755 --- a/tutorials/Image_And_Icons_Manipulations/README.md +++ b/tutorials/Image_And_Icons_Manipulations/README.md @@ -6,128 +6,186 @@ In this tutorial we will show you how to work with images using Compose for Desk ## Loading images from resources -Using images from application resources is very simple. Suppose we have a PNG image that is placed in the `resources/images` directory in our project. For this tutorial we will use the image sample: +Using images from application resources is very simple. Suppose we have a PNG image that is placed in the `resources` directory in our project. For this tutorial we will use the image sample: ![Sample](sample.png) ```kotlin -import androidx.compose.desktop.Window import androidx.compose.foundation.Image import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier -import androidx.compose.ui.res.imageResource - -fun main() { - Window { - Image( - bitmap = imageResource("images/sample.png"), // ImageBitmap - contentDescription = "Sample", - modifier = Modifier.fillMaxSize() - ) - } +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.window.singleWindowApplication + +@OptIn(ExperimentalComposeUiApi::class) +fun main() = singleWindowApplication { + Image( + painter = painterResource("sample.png"), // ImageBitmap + contentDescription = "Sample", + modifier = Modifier.fillMaxSize() + ) } ``` +`painterResource` supports raster (BMP, GIF, HEIF, ICO, JPEG, PNG, WBMP, WebP) and vector formats (SVG, [XML vector drawable](https://developer.android.com/guide/topics/graphics/vector-drawable-resources)). + ![Resources](image_from_resources.png) -## Loading images from device storage +## Loading images from device storage asynchronously -To create an `ImageBitmap` from a loaded image stored in the device memory you can use `org.jetbrains.skija.Image`: +To load an image stored in the device memory you can use `loadImageBitmap`, `loadSvgPainter` or `loadXmlImageVector`. The example below shows how to use them to load an image asynchronously. ```kotlin -import androidx.compose.desktop.Window import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.width +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.produceState import androidx.compose.runtime.remember +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.ImageBitmap -import androidx.compose.ui.graphics.asImageBitmap -import org.jetbrains.skija.Image +import androidx.compose.ui.graphics.painter.BitmapPainter +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.loadImageBitmap +import androidx.compose.ui.res.loadSvgPainter +import androidx.compose.ui.res.loadXmlImageVector +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.singleWindowApplication +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.xml.sax.InputSource import java.io.File - -fun main() { - Window { - val image = remember { imageFromFile(File("sample.png")) } - Image( - bitmap = image, +import java.io.IOException + +@OptIn(ExperimentalComposeUiApi::class) +fun main() = singleWindowApplication { + val density = LocalDensity.current + Column { + AsyncImage( + load = { loadImageBitmap(File("sample.png")) }, + painterFor = { remember { BitmapPainter(it) } }, contentDescription = "Sample", - modifier = Modifier.fillMaxSize() + modifier = Modifier.width(200.dp) + ) + AsyncImage( + load = { loadSvgPainter(File("idea-logo.svg"), density) }, + painterFor = { it }, + contentDescription = "Idea logo", + contentScale = ContentScale.FillWidth, + modifier = Modifier.width(200.dp) + ) + AsyncImage( + load = { loadXmlImageVector(File("compose-logo.xml"), density) }, + painterFor = { rememberVectorPainter(it) }, + contentDescription = "Compose logo", + contentScale = ContentScale.FillWidth, + modifier = Modifier.width(200.dp) ) } } -fun imageFromFile(file: File): ImageBitmap { - return Image.makeFromEncoded(file.readBytes()).asImageBitmap() +@Composable +fun AsyncImage( + load: suspend () -> T, + painterFor: @Composable (T) -> Painter, + contentDescription: String, + modifier: Modifier = Modifier, + contentScale: ContentScale = ContentScale.Fit, +) { + val image: T? by produceState(null) { + value = withContext(Dispatchers.IO) { + try { + load() + } catch (e: IOException) { + e.printStackTrace() + null + } + } + } + + if (image != null) { + Image( + painter = painterFor(image!!), + contentDescription = contentDescription, + contentScale = contentScale, + modifier = modifier + ) + } } + +fun loadImageBitmap(file: File): ImageBitmap = + file.inputStream().buffered().use(::loadImageBitmap) + +fun loadSvgPainter(file: File, density: Density): Painter = + file.inputStream().buffered().use { loadSvgPainter(it, density) } + +fun loadXmlImageVector(file: File, density: Density): ImageVector = + file.inputStream().buffered().use { loadXmlImageVector(InputSource(it), density) } ``` -![Storage](image_from_resources.png) +![Storage](image_from_resources2.png) + +[PNG](sample.png) + +[SVG](../../artwork/idea-logo.svg) + +[XML vector drawable](../../artwork/compose-logo.xml) ## Drawing raw image data using native canvas You may want to draw raw image data, in which case you can use `Canvas` and` drawIntoCanvas`. ```kotlin -import androidx.compose.desktop.Window import androidx.compose.foundation.Canvas import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.remember +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.Paint +import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.graphics.drawscope.drawIntoCanvas -import androidx.compose.ui.graphics.nativeCanvas +import androidx.compose.ui.res.loadImageBitmap +import androidx.compose.ui.res.useResource +import androidx.compose.ui.window.singleWindowApplication import org.jetbrains.skija.Bitmap import org.jetbrains.skija.ColorAlphaType -import org.jetbrains.skija.IRect import org.jetbrains.skija.ImageInfo -import org.jetbrains.skija.Image -import java.awt.image.BufferedImage -import java.io.File -import javax.imageio.ImageIO -fun main() { - Window { - val bitmap = remember { bitmapFromByteArray() } - Canvas( - modifier = Modifier.fillMaxSize() - ) { - drawIntoCanvas { canvas -> - canvas.nativeCanvas.drawImageRect( - Image.makeFromBitmap(bitmap), - IRect(0, 0, bitmap.getWidth(), bitmap.getHeight()).toRect() - ) - } +private val sample = useResource("sample.png", ::loadImageBitmap) + +@OptIn(ExperimentalComposeUiApi::class) +fun main() = singleWindowApplication { + val bitmap = remember { bitmapFromByteArray(sample.getBytes(), sample.width, sample.height) } + Canvas( + modifier = Modifier.fillMaxSize() + ) { + drawIntoCanvas { canvas -> + canvas.drawImage(bitmap, Offset.Zero, Paint()) } } } -fun bitmapFromByteArray(): Bitmap { - var image: BufferedImage? = null - try { - image = ImageIO.read(File("sample.png")) - } catch (e: Exception) { - // image file does not exist - } - - if (image == null) { - image = BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB) - } - val pixels = getBytes(image) // assume we only have raw pixels - - // allocate and fill skija Bitmap +fun bitmapFromByteArray(pixels: ByteArray, width: Int, height: Int): ImageBitmap { val bitmap = Bitmap() - bitmap.allocPixels(ImageInfo.makeS32(image.width, image.height, ColorAlphaType.PREMUL)) - bitmap.installPixels(bitmap.getImageInfo(), pixels, (image.width * 4).toLong()) - - return bitmap + bitmap.allocPixels(ImageInfo.makeS32(width, height, ColorAlphaType.PREMUL)) + bitmap.installPixels(bitmap.imageInfo, pixels, (width * 4).toLong()) + return bitmap.asImageBitmap() } // creating byte array from BufferedImage -private fun getBytes(image: BufferedImage): ByteArray { - val width = image.width - val height = image.height - +private fun ImageBitmap.getBytes(): ByteArray { val buffer = IntArray(width * height) - image.getRGB(0, 0, width, height, buffer, 0, width) + readPixels(buffer) val pixels = ByteArray(width * height * 4) @@ -150,111 +208,27 @@ private fun getBytes(image: BufferedImage): ByteArray { ## Setting the application window icon -You have 2 ways to set icon for window: -1. Via parameter in `Window` function (or in `AppWindow` constructor) +You can set the icon for the window via parameter in the `Window` function. + +Note that to change the icon on the taskbar on some OS (macOs), you should change icon in [build.gradle](https://github.com/JetBrains/compose-jb/tree/sync/2021-07-23/tutorials/Native_distributions_and_local_execution#app-icon) ```kotlin -import androidx.compose.desktop.Window -import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.ImageBitmap -import androidx.compose.ui.graphics.asImageBitmap -import org.jetbrains.skija.Image -import java.awt.image.BufferedImage -import java.io.ByteArrayOutputStream -import java.io.File -import javax.imageio.ImageIO +import androidx.compose.ui.draw.paint +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.window.Window +import androidx.compose.ui.window.application -fun main() { - val image = getWindowIcon() +fun main() = application { + val icon = painterResource("sample.png") Window( - icon = image + onCloseRequest = ::exitApplication, + icon = icon ) { - val imageAsset = remember { asImageAsset(image) } - Image( - bitmap = imageAsset, - contentDescription = "Icon", - modifier = Modifier.fillMaxSize() - ) - } -} - -fun getWindowIcon(): BufferedImage { - var image: BufferedImage? = null - try { - image = ImageIO.read(File("sample.png")) - } catch (e: Exception) { - // image file does not exist - } - - 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() - ) + Box(Modifier.paint(icon).fillMaxSize()) } - - AppManager.focusedWindow?.setIcon(image) -} - -fun getWindowIcon(): BufferedImage { - var image: BufferedImage? = null - try { - image = ImageIO.read(File("sample.png")) - } catch (e: Exception) { - // image file does not exist - } - - if (image == null) { - image = BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB) - } - - return image -} - -fun asImageAsset(image: BufferedImage): ImageBitmap { - val baos = ByteArrayOutputStream() - ImageIO.write(image, "png", baos) - - return Image.makeFromEncoded(baos.toByteArray()).asImageBitmap() } ``` @@ -265,131 +239,32 @@ fun asImageAsset(image: BufferedImage): ImageBitmap { You can create a tray icon for your application: ```kotlin -import androidx.compose.desktop.AppManager -import androidx.compose.desktop.Window import androidx.compose.foundation.Image import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.runtime.DisposableEffect import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.ImageBitmap -import androidx.compose.ui.graphics.asImageBitmap -import androidx.compose.ui.window.v1.MenuItem -import androidx.compose.ui.window.v1.Tray -import org.jetbrains.skija.Image -import java.awt.image.BufferedImage -import java.io.ByteArrayOutputStream -import java.io.File -import javax.imageio.ImageIO - -fun main() { - val image = getWindowIcon() - Window { - DisposableEffect(Unit) { - val tray = Tray().apply { - icon(getWindowIcon()) - menu( - MenuItem( - name = "Quit App", - onClick = { AppManager.exit() } - ) - ) - } - onDispose { - tray.remove() - } +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.window.Tray +import androidx.compose.ui.window.Window +import androidx.compose.ui.window.application + +fun main() = application { + val icon = painterResource("sample.png") + + Tray( + icon = icon, + menu = { + Item("Quit App", onClick = ::exitApplication) } + ) - val imageAsset = asImageAsset(image) + Window(onCloseRequest = ::exitApplication, icon = icon) { Image( - bitmap = imageAsset, + painter = icon, contentDescription = "Icon", 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) \ No newline at end of file diff --git a/tutorials/Image_And_Icons_Manipulations/image_from_resources2.png b/tutorials/Image_And_Icons_Manipulations/image_from_resources2.png new file mode 100644 index 0000000000..243ad6b9e3 Binary files /dev/null and b/tutorials/Image_And_Icons_Manipulations/image_from_resources2.png differ diff --git a/tutorials/Image_And_Icons_Manipulations/loading_svg_images.png b/tutorials/Image_And_Icons_Manipulations/loading_svg_images.png deleted file mode 100644 index b27c48b670..0000000000 Binary files a/tutorials/Image_And_Icons_Manipulations/loading_svg_images.png and /dev/null differ diff --git a/tutorials/Image_And_Icons_Manipulations/loading_xml_vector_images.png b/tutorials/Image_And_Icons_Manipulations/loading_xml_vector_images.png deleted file mode 100644 index 2c716a45b9..0000000000 Binary files a/tutorials/Image_And_Icons_Manipulations/loading_xml_vector_images.png and /dev/null differ diff --git a/tutorials/Image_And_Icons_Manipulations/window_icon.png b/tutorials/Image_And_Icons_Manipulations/window_icon.png index d4089bf8b0..7dd2db0120 100755 Binary files a/tutorials/Image_And_Icons_Manipulations/window_icon.png and b/tutorials/Image_And_Icons_Manipulations/window_icon.png differ diff --git a/tutorials/Keyboard/README.md b/tutorials/Keyboard/README.md index ee94c9abe6..ad9a37a0e4 100644 --- a/tutorials/Keyboard/README.md +++ b/tutorials/Keyboard/README.md @@ -22,25 +22,27 @@ It works the same as Compose for Android, for details see [API Reference](https: The most common use case is to define keyboard handlers for active controls like `TextField`. You can use both `onKeyEvent` and `onPreviewKeyEvent` but the last one is usually preferable to define shortcuts while it guarantees you that key events will not be consumed by children components. Here is an example: ```kotlin -import androidx.compose.desktop.Window import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material.Text import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text import androidx.compose.material.TextField +import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.input.key.* -import androidx.compose.ui.unit.IntSize -import androidx.compose.ui.unit.dp -import androidx.compose.runtime.getValue import androidx.compose.runtime.setValue import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.key.Key +import androidx.compose.ui.input.key.isCtrlPressed +import androidx.compose.ui.input.key.key +import androidx.compose.ui.input.key.onPreviewKeyEvent +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.singleWindowApplication @OptIn(ExperimentalComposeUiApi::class) -fun main() = Window(title = "Compose for Desktop", size = IntSize(300, 300)) { +fun main() = singleWindowApplication { MaterialTheme { var consumedText by remember { mutableStateOf(0) } var text by remember { mutableStateOf("") } @@ -51,12 +53,12 @@ fun main() = Window(title = "Compose for Desktop", size = IntSize(300, 300)) { onValueChange = { text = it }, modifier = Modifier.onPreviewKeyEvent { when { - (it.isMetaPressed && it.key == Key.Minus) -> { + (it.isCtrlPressed && it.key == Key.Minus) -> { consumedText -= text.length text = "" true } - (it.isMetaPressed && it.key == Key.Equals) -> { + (it.isCtrlPressed && it.key == Key.Equals) -> { consumedText += text.length text = "" true @@ -70,19 +72,15 @@ fun main() = Window(title = "Compose for Desktop", size = IntSize(300, 300)) { } ``` - -Note the annotation `@OptIn(ExperimentalKeyInput::class)`. Some keys related APIs are still an experimental feature of Compose, and later API changes are possible. So it requires the use of a special annotation to emphasize the experimental nature of the code. +Note the annotation `@OptIn(ExperimentalComposeUiApi::class)`. Some keys related APIs are still an experimental feature of Compose, and later API changes are possible. So it requires the use of a special annotation to emphasize the experimental nature of the code. ![keyInputFilter](keyInputFilter.gif) ## Window-scoped events -`LocalAppWindow` instances have a `keyboard` property. It is possible to use it to define keyboard event handlers that are always active in the current window. You also can get window instance for popups. Again, you possibly want to use `onPreviewKeyEvent` here to intercept events. Here is an example: +`Window`,`singleWindowApplication` and `Dialog` functions have a `onPreviewKeyEvent` and a `onKeyEvent` properties. It is possible to use them to define keyboard event handlers that are always active in the current window. You possibly want to use `onPreviewKeyEvent` here to intercept events. Here is an example: ``` kotlin -import androidx.compose.desktop.AppWindow -import androidx.compose.desktop.LocalAppWindow -import androidx.compose.desktop.Window import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize @@ -91,32 +89,41 @@ import androidx.compose.material.Button import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.input.key.Key -import androidx.compose.ui.input.key.isMetaPressed +import androidx.compose.ui.input.key.KeyEventType +import androidx.compose.ui.input.key.isCtrlPressed import androidx.compose.ui.input.key.isShiftPressed import androidx.compose.ui.input.key.key -import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.input.key.type import androidx.compose.ui.unit.dp -import androidx.compose.runtime.getValue -import androidx.compose.runtime.setValue +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.singleWindowApplication + +private var cleared by mutableStateOf(false) @OptIn(ExperimentalComposeUiApi::class) -fun main() = Window(title = "Compose for Desktop", size = IntSize(300, 300)) { - MaterialTheme { - var cleared by remember { mutableStateOf(false) } - LocalAppWindow.current.keyboard.onKeyEvent = { - if (it.isMetaPressed && it.isShiftPressed && it.key == Key.C) { - cleared = true - true - } else { - false - } +fun main() = singleWindowApplication( + onKeyEvent = { + if ( + it.isCtrlPressed && + it.isShiftPressed && + it.key == Key.C && + it.type == KeyEventType.KeyDown + ) { + cleared = true + true + } else { + false } - + } +) { + MaterialTheme { if (cleared) { Text("The App was cleared!") } else { @@ -128,25 +135,29 @@ fun main() = Window(title = "Compose for Desktop", size = IntSize(300, 300)) { @OptIn(ExperimentalComposeUiApi::class) @Composable fun App() { + 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)) { Button( modifier = Modifier.padding(4.dp), - onClick = { - 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!") - } - } + onClick = { isDialogOpen = true } ) { - Text("Open popup") + Text("Open dialog") } } } diff --git a/tutorials/Mouse_Events/README.md b/tutorials/Mouse_Events/README.md index 5d767897c7..26a4b00b85 100644 --- a/tutorials/Mouse_Events/README.md +++ b/tutorials/Mouse_Events/README.md @@ -13,23 +13,27 @@ Click listeners are available in both Compose on Android and Compose for Desktop so code like this will work on both platforms: ```kotlin -import androidx.compose.desktop.Window import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.combinedClickable -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.material.Text import androidx.compose.runtime.getValue -import androidx.compose.runtime.setValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.singleWindowApplication -fun main() = Window(title = "Compose for Desktop", size = IntSize(400, 400)) { +@OptIn(ExperimentalComposeUiApi::class) +fun main() = singleWindowApplication { var count by remember { mutableStateOf(0) } Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxWidth()) { var text by remember { mutableStateOf("Click magenta box!") } @@ -67,20 +71,23 @@ the following code will only work with Compose for Desktop. Let's create a window and install a pointer move filter on it that changes the background color according to the mouse pointer position: ```kotlin -import androidx.compose.desktop.Window import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.runtime.getValue -import androidx.compose.runtime.setValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.pointer.pointerMoveFilter -import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.window.singleWindowApplication -fun main() = Window(title = "Compose for Desktop", size = IntSize(400, 400)) { +@OptIn(ExperimentalComposeUiApi::class) +fun main() = singleWindowApplication { var color by remember { mutableStateOf(Color(0, 0, 0)) } Box( modifier = Modifier @@ -103,23 +110,26 @@ fun main() = Window(title = "Compose for Desktop", size = IntSize(400, 400)) { Compose for Desktop also supports pointer enter and exit handlers, like this: ```kotlin -import androidx.compose.desktop.Window import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.material.Text import androidx.compose.runtime.getValue -import androidx.compose.runtime.setValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.pointer.pointerMoveFilter import androidx.compose.ui.text.font.FontStyle -import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.singleWindowApplication -fun main() = Window(title = "Compose for Desktop", size = IntSize(400, 400)) { +@OptIn(ExperimentalComposeUiApi::class) +fun main() = singleWindowApplication { Column( Modifier.background(Color.White), verticalArrangement = Arrangement.spacedBy(10.dp) @@ -155,19 +165,19 @@ fun main() = Window(title = "Compose for Desktop", size = IntSize(400, 400)) { Compose for Desktop contains desktop-only `Modifier.mouseClickable`, where data about pressed mouse buttons and keyboard modifiers is available. This is an experimental API, which means that it's likely to be changed before release. ```kotlin -import androidx.compose.desktop.Window import androidx.compose.foundation.ExperimentalDesktopApi import androidx.compose.foundation.mouseClickable import androidx.compose.material.Text +import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.getValue import androidx.compose.runtime.setValue +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.window.singleWindowApplication -@OptIn(ExperimentalDesktopApi::class) -fun main() = Window(title = "Compose for Desktop", size = IntSize(400, 400)) { +@OptIn(ExperimentalComposeUiApi::class, ExperimentalDesktopApi::class) +fun main() = singleWindowApplication { var clickableText by remember { mutableStateOf("Click me!") } Text( diff --git a/tutorials/Native_distributions_and_local_execution/README.md b/tutorials/Native_distributions_and_local_execution/README.md index dc4f55863d..c2cffcac71 100755 --- a/tutorials/Native_distributions_and_local_execution/README.md +++ b/tutorials/Native_distributions_and_local_execution/README.md @@ -459,24 +459,28 @@ val macExtraPlistKeys: String ``` kotlin // src/main/main.kt -import androidx.compose.desktop.Window -import androidx.compose.material.Text import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.window.singleWindowApplication import java.awt.Desktop +@OptIn(ExperimentalComposeUiApi::class) fun main() { - val text = mutableStateOf("Hello, World!") + var text by mutableStateOf("Hello, World!") - Desktop.getDesktop().setOpenURIHandler { event -> - text.value = "Open URI: " + event.uri + try { + Desktop.getDesktop().setOpenURIHandler { event -> + text = "Open URI: " + event.uri + } + } catch (e: UnsupportedOperationException) { + println("setOpenURIHandler is unsupported") } - Window { - var text by remember { text } + singleWindowApplication { MaterialTheme { Text(text) } diff --git a/tutorials/Navigation/README.md b/tutorials/Navigation/README.md index 9306d8d22e..ea5b3239d8 100644 --- a/tutorials/Navigation/README.md +++ b/tutorials/Navigation/README.md @@ -289,21 +289,23 @@ Application and Root initialisation: ``` kotlin import androidx.compose.desktop.DesktopTheme -import androidx.compose.desktop.Window import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material.MaterialTheme import androidx.compose.material.Surface import androidx.compose.runtime.Composable +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier +import androidx.compose.ui.window.singleWindowApplication import com.arkivanov.decompose.extensions.compose.jetbrains.rememberRootComponent -fun main() { - Window("Navigation tutorial") { - Surface(modifier = Modifier.fillMaxSize()) { - MaterialTheme { - DesktopTheme { - RootUi(root()) // Render the Root and its children - } +@OptIn(ExperimentalComposeUiApi::class) +fun main() = singleWindowApplication( + title = "Navigation tutorial" +) { + Surface(modifier = Modifier.fillMaxSize()) { + MaterialTheme { + DesktopTheme { + RootUi(root()) // Render the Root and its children } } } diff --git a/tutorials/Swing_Integration/README.md b/tutorials/Swing_Integration/README.md index 34bea20745..ea6bafb25e 100644 --- a/tutorials/Swing_Integration/README.md +++ b/tutorials/Swing_Integration/README.md @@ -9,34 +9,31 @@ In this tutorial, we will show you how to use ComposePanel and SwingPanel in you ComposePanel lets you create a UI using Compose for Desktop in a Swing-based UI. To achieve this you need to create an instance of ComposePanel, add it to your Swing layout, and describe the composition inside `setContent`. You may also need to clear the CFD application events via `AppManager.setEvents`. ```kotlin -import androidx.compose.desktop.AppManager -import androidx.compose.desktop.ComposePanel import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.width -import androidx.compose.material.Text import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Button import androidx.compose.material.Surface +import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.Alignment -import androidx.compose.ui.graphics.Color import androidx.compose.ui.Modifier +import androidx.compose.ui.awt.ComposePanel +import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import java.awt.BorderLayout import java.awt.Dimension -import java.awt.event.ActionEvent -import java.awt.event.ActionListener -import javax.swing.JFrame import javax.swing.JButton +import javax.swing.JFrame import javax.swing.SwingUtilities import javax.swing.WindowConstants @@ -44,17 +41,7 @@ val northClicks = mutableStateOf(0) val westClicks = mutableStateOf(0) val eastClicks = mutableStateOf(0) -fun main() { - // explicitly clear the application events - AppManager.setEvents( - onAppStart = null, - onAppExit = null, - onWindowsEmpty = null - ) - SwingComposeWindow() -} - -fun SwingComposeWindow() = SwingUtilities.invokeLater { +fun main() = SwingUtilities.invokeLater { val window = JFrame() // creating ComposePanel @@ -62,10 +49,9 @@ fun SwingComposeWindow() = SwingUtilities.invokeLater { window.defaultCloseOperation = WindowConstants.EXIT_ON_CLOSE window.title = "SwingComposeWindow" - - window.contentPane.add(actionButton("NORTH", { northClicks.value++ }), BorderLayout.NORTH) - window.contentPane.add(actionButton("WEST", { westClicks.value++ }), BorderLayout.WEST) - window.contentPane.add(actionButton("EAST", { eastClicks.value++ }), BorderLayout.EAST) + window.contentPane.add(actionButton("NORTH", action = { northClicks.value++ }), BorderLayout.NORTH) + window.contentPane.add(actionButton("WEST", action = { westClicks.value++ }), BorderLayout.WEST) + window.contentPane.add(actionButton("EAST", action = { eastClicks.value++ }), BorderLayout.EAST) window.contentPane.add( actionButton( text = "SOUTH/REMOVE COMPOSE", @@ -85,19 +71,14 @@ fun SwingComposeWindow() = SwingUtilities.invokeLater { } window.setSize(800, 600) - window.setVisible(true) + window.isVisible = true } -fun actionButton(text: String, action: (() -> Unit)? = null): JButton { +fun actionButton(text: String, action: () -> Unit): JButton { val button = JButton(text) - button.setToolTipText("Tooltip for $text button.") - button.setPreferredSize(Dimension(100, 100)) - button.addActionListener(object : ActionListener { - public override fun actionPerformed(e: ActionEvent) { - action?.invoke() - } - }) - + button.toolTipText = "Tooltip for $text button." + button.preferredSize = Dimension(100, 100) + button.addActionListener { action() } return button } @@ -147,13 +128,11 @@ fun Counter(text: String, counter: MutableState) { ![IntegrationWithSwing](screenshot.png) -## Adding a Swing component to CFD composition using SwingPanel. +## Adding a Swing component to CFD composition using SwingPanel SwingPanel lets you create a UI using Swing in a Compose-based UI. To achieve this you need to create Swing `JComponent` in the `factory` parameter of `SwingPanel`. ```kotlin -import androidx.compose.desktop.SwingPanel -import androidx.compose.desktop.Window import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize @@ -168,57 +147,56 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier +import androidx.compose.ui.awt.SwingPanel import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.singleWindowApplication import java.awt.Component -import java.awt.Dimension -import java.awt.event.ActionEvent -import java.awt.event.ActionListener import javax.swing.BoxLayout import javax.swing.JButton import javax.swing.JPanel -fun main() { - Window { - val counter = remember { mutableStateOf(0) } +@OptIn(ExperimentalComposeUiApi::class) +fun main() = singleWindowApplication { + val counter = remember { mutableStateOf(0) } - val inc: () -> Unit = { counter.value++ } - val dec: () -> Unit = { counter.value-- } + val inc: () -> Unit = { counter.value++ } + val dec: () -> Unit = { counter.value-- } - Box( - modifier = Modifier.fillMaxWidth().height(60.dp).padding(top = 20.dp), - contentAlignment = Alignment.Center - ) { - Text("Counter: ${counter.value}") - } - - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center + Box( + modifier = Modifier.fillMaxWidth().height(60.dp).padding(top = 20.dp), + contentAlignment = Alignment.Center + ) { + Text("Counter: ${counter.value}") + } + + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Column( + modifier = Modifier.padding(top = 80.dp, bottom = 20.dp) ) { - Column( - modifier = Modifier.padding(top = 80.dp, bottom = 20.dp) - ) { - Button("1. Compose Button: increment", inc) - Spacer(modifier = Modifier.height(20.dp)) - - SwingPanel( - background = Color.White, - modifier = Modifier.size(270.dp, 90.dp), - factory = { - JPanel().apply { - setLayout(BoxLayout(this, BoxLayout.Y_AXIS)) - add(actionButton("1. Swing Button: decrement", dec)) - add(actionButton("2. Swing Button: decrement", dec)) - add(actionButton("3. Swing Button: decrement", dec)) - } + Button("1. Compose Button: increment", inc) + Spacer(modifier = Modifier.height(20.dp)) + + SwingPanel( + background = Color.White, + modifier = Modifier.size(270.dp, 90.dp), + factory = { + JPanel().apply { + layout = BoxLayout(this, BoxLayout.Y_AXIS) + add(actionButton("1. Swing Button: decrement", dec)) + add(actionButton("2. Swing Button: decrement", dec)) + add(actionButton("3. Swing Button: decrement", dec)) } - ) + } + ) - Spacer(modifier = Modifier.height(20.dp)) - Button("2. Compose Button: increment", inc) - } + Spacer(modifier = Modifier.height(20.dp)) + Button("2. Compose Button: increment", inc) } } } @@ -235,15 +213,11 @@ fun Button(text: String = "", action: (() -> Unit)? = null) { fun actionButton( text: String, - action: (() -> Unit)? = null + action: () -> Unit ): JButton { val button = JButton(text) - button.setAlignmentX(Component.CENTER_ALIGNMENT) - button.addActionListener(object : ActionListener { - public override fun actionPerformed(e: ActionEvent) { - action?.invoke() - } - }) + button.alignmentX = Component.CENTER_ALIGNMENT + button.addActionListener { action() } return button } diff --git a/tutorials/Tray_Notifications_MenuBar/README.md b/tutorials/Tray_Notifications_MenuBar/README.md deleted file mode 100755 index 582d132445..0000000000 --- a/tutorials/Tray_Notifications_MenuBar/README.md +++ /dev/null @@ -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) diff --git a/tutorials/Tray_Notifications_MenuBar/app_menubar.gif b/tutorials/Tray_Notifications_MenuBar/app_menubar.gif deleted file mode 100644 index 6f186df333..0000000000 Binary files a/tutorials/Tray_Notifications_MenuBar/app_menubar.gif and /dev/null differ diff --git a/tutorials/Tray_Notifications_MenuBar/notifier.gif b/tutorials/Tray_Notifications_MenuBar/notifier.gif deleted file mode 100644 index 9ab6638f77..0000000000 Binary files a/tutorials/Tray_Notifications_MenuBar/notifier.gif and /dev/null differ diff --git a/tutorials/Tray_Notifications_MenuBar/tray.gif b/tutorials/Tray_Notifications_MenuBar/tray.gif deleted file mode 100644 index 4f42bcb2a1..0000000000 Binary files a/tutorials/Tray_Notifications_MenuBar/tray.gif and /dev/null differ diff --git a/tutorials/Tray_Notifications_MenuBar/window_menubar.gif b/tutorials/Tray_Notifications_MenuBar/window_menubar.gif deleted file mode 100644 index 8c08c60e28..0000000000 Binary files a/tutorials/Tray_Notifications_MenuBar/window_menubar.gif and /dev/null differ diff --git a/tutorials/Tray_Notifications_MenuBar_new/README.md b/tutorials/Tray_Notifications_MenuBar_new/README.md index 0d6f7b9744..e1efc1e83f 100644 --- a/tutorials/Tray_Notifications_MenuBar_new/README.md +++ b/tutorials/Tray_Notifications_MenuBar_new/README.md @@ -16,22 +16,23 @@ You can add an application icon to the system tray. You can also send notificati import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material.Text +import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.getValue import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment -import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.window.Notification import androidx.compose.ui.window.Tray import androidx.compose.ui.window.Window import androidx.compose.ui.window.application import androidx.compose.ui.window.rememberTrayState -import java.awt.Color -import java.awt.image.BufferedImage -@OptIn(ExperimentalComposeUiApi::class) fun main() = application { var count by remember { mutableStateOf(0) } var isOpen by remember { mutableStateOf(true) } @@ -39,13 +40,13 @@ fun main() = application { if (isOpen) { Window( onCloseRequest = ::exitApplication, - icon = remember { getMyAppIcon() } + icon = MyAppIcon ) { val trayState = rememberTrayState() val notification = Notification("Notification", "Message from MyApp!") Tray( state = trayState, - icon = remember { getTrayIcon() }, + icon = TrayIcon, menu = { Item( "Increment value", @@ -73,34 +74,28 @@ fun main() = application { modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center ) { - Text(text = "Value: ${count}") + Text(text = "Value: $count") } } } } -fun getMyAppIcon(): BufferedImage { - val size = 256 - val image = BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB) - val graphics = image.createGraphics() - graphics.color = Color.green - graphics.fillOval(size / 4, 0, size / 2, size) - graphics.color = Color.blue - graphics.fillOval(0, size / 4, size, size / 2) - graphics.color = Color.red - graphics.fillOval(size / 4, size / 4, size / 2, size / 2) - graphics.dispose() - return image +object MyAppIcon : Painter() { + override val intrinsicSize = Size(256f, 256f) + + override fun DrawScope.onDraw() { + drawOval(Color.Green, Offset(size.width / 4, 0f), Size(size.width / 2f, size.height)) + drawOval(Color.Blue, Offset(0f, size.height / 4), Size(size.width, size.height / 2f)) + drawOval(Color.Red, Offset(size.width / 4, size.height / 4), Size(size.width / 2f, size.height / 2f)) + } } -fun getTrayIcon(): BufferedImage { - val size = 256 - val image = BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB) - val graphics = image.createGraphics() - graphics.color = Color.orange - graphics.fillOval(0, 0, size, size) - graphics.dispose() - return image +object TrayIcon : Painter() { + override val intrinsicSize = Size(256f, 256f) + + override fun DrawScope.onDraw() { + drawOval(Color(0xFFFFA500)) + } } ``` @@ -121,57 +116,67 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.input.key.Key +import androidx.compose.ui.input.key.KeyShortcut import androidx.compose.ui.window.MenuBar import androidx.compose.ui.window.Window import androidx.compose.ui.window.application @OptIn(ExperimentalComposeUiApi::class) -fun main() { - // Currently we use Swing's menu under the hood, so we need to set this property to change the look and feel of the menu on Windows/Linux - System.setProperty("skiko.rendering.laf.global", "true") - - application { - var action by remember { mutableStateOf("Last action: None") } - var isOpen by remember { mutableStateOf(true) } - - if (isOpen) { - var isSubmenuShowing by remember { mutableStateOf(false) } - - Window(onCloseRequest = { isOpen = false }) { - MenuBar { - Menu("Actions") { - Item( - if (isSubmenuShowing) "Hide advanced settings" else "Show advanced settings", - onClick = { - isSubmenuShowing = !isSubmenuShowing - } - ) - if (isSubmenuShowing) { - Menu("Settings") { - Item("Setting 1", onClick = { action = "Last action: Setting 1" }) - Item("Setting 2", onClick = { action = "Last action: Setting 2" }) - } +fun main() = application { + var action by remember { mutableStateOf("Last action: None") } + var isOpen by remember { mutableStateOf(true) } + + if (isOpen) { + var isSubmenuShowing by remember { mutableStateOf(false) } + + Window(onCloseRequest = { isOpen = false }) { + MenuBar { + Menu("File", mnemonic = 'F') { + Item("Copy", onClick = { action = "Last action: Copy" }, shortcut = KeyShortcut(Key.C, ctrl = true)) + Item("Paste", onClick = { action = "Last action: Paste" }, shortcut = KeyShortcut(Key.V, ctrl = true)) + } + Menu("Actions", mnemonic = 'A') { + CheckboxItem( + "Advanced settings", + checked = isSubmenuShowing, + onCheckedChange = { + isSubmenuShowing = !isSubmenuShowing + } + ) + if (isSubmenuShowing) { + Menu("Settings") { + 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( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center - ) { - Text(text = action) - } + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Text(text = action) } } } } + +object TrayIcon : Painter() { + override val intrinsicSize = Size(256f, 256f) + + override fun DrawScope.onDraw() { + drawOval(Color(0xFFFFA500)) + } +} ``` ![](window_menubar.gif) diff --git a/tutorials/Window_API/README.md b/tutorials/Window_API/README.md deleted file mode 100755 index c673e1599d..0000000000 --- a/tutorials/Window_API/README.md +++ /dev/null @@ -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) diff --git a/tutorials/Window_API/center_the_window.gif b/tutorials/Window_API/center_the_window.gif deleted file mode 100644 index 9b8eb0aeda..0000000000 Binary files a/tutorials/Window_API/center_the_window.gif and /dev/null differ diff --git a/tutorials/Window_API/current_window.gif b/tutorials/Window_API/current_window.gif deleted file mode 100644 index dc0807761b..0000000000 Binary files a/tutorials/Window_API/current_window.gif and /dev/null differ diff --git a/tutorials/Window_API/focus_the_window.gif b/tutorials/Window_API/focus_the_window.gif deleted file mode 100644 index 2d62993bb4..0000000000 Binary files a/tutorials/Window_API/focus_the_window.gif and /dev/null differ diff --git a/tutorials/Window_API/scaling_factor.jpg b/tutorials/Window_API/scaling_factor.jpg deleted file mode 100644 index 6faa9eda03..0000000000 Binary files a/tutorials/Window_API/scaling_factor.jpg and /dev/null differ diff --git a/tutorials/Window_API/window_attr.gif b/tutorials/Window_API/window_attr.gif deleted file mode 100644 index a0d59d1c25..0000000000 Binary files a/tutorials/Window_API/window_attr.gif and /dev/null differ diff --git a/tutorials/Window_API/window_state.gif b/tutorials/Window_API/window_state.gif deleted file mode 100644 index d037300757..0000000000 Binary files a/tutorials/Window_API/window_state.gif and /dev/null differ diff --git a/tutorials/Window_API_new/README.md b/tutorials/Window_API_new/README.md index 947d93de47..6659ac8541 100644 --- a/tutorials/Window_API_new/README.md +++ b/tutorials/Window_API_new/README.md @@ -13,11 +13,9 @@ Top-level windows can be conditionally created in other composable functions and The main function for creating windows is `Window`. This function should be used in a Composable scope. The easiest way to create a Composable scope is to use the `application` function: ```kotlin -import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.window.Window import androidx.compose.ui.window.application -@OptIn(ExperimentalComposeUiApi::class) fun main() = application { Window(onCloseRequest = ::exitApplication) { // Content @@ -33,11 +31,9 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.window.Window import androidx.compose.ui.window.application -@OptIn(ExperimentalComposeUiApi::class) fun main() = application { var fileName by remember { mutableStateOf("Untitled") } @@ -61,18 +57,18 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.window.Window import androidx.compose.ui.window.application import kotlinx.coroutines.delay -@OptIn(ExperimentalComposeUiApi::class) fun main() = application { var isPerformingTask by remember { mutableStateOf(true) } + LaunchedEffect(Unit) { delay(2000) // Do some heavy lifting isPerformingTask = false } + if (isPerformingTask) { Window(onCloseRequest = ::exitApplication) { Text("Performing some tasks. Please wait!") @@ -96,16 +92,14 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.Window import androidx.compose.ui.window.application -@OptIn(ExperimentalComposeUiApi::class) fun main() = application { var isOpen by remember { mutableStateOf(true) } var isAskingToClose by remember { mutableStateOf(false) } - + if (isOpen) { Window( onCloseRequest = { isAskingToClose = true } @@ -136,16 +130,15 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.window.Tray import androidx.compose.ui.window.Window import androidx.compose.ui.window.application -import androidx.compose.ui.window.rememberWindowState import kotlinx.coroutines.delay -import java.awt.Color -import java.awt.image.BufferedImage -@OptIn(ExperimentalComposeUiApi::class) fun main() = application { var isVisible by remember { mutableStateOf(true) } @@ -166,7 +159,7 @@ fun main() = application { if (!isVisible) { Tray( - remember { getTrayIcon() }, + TrayIcon, hint = "Counter", onAction = { isVisible = true }, menu = { @@ -176,14 +169,12 @@ fun main() = application { } } -fun getTrayIcon(): BufferedImage { - val size = 256 - val image = BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB) - val graphics = image.createGraphics() - graphics.color = Color.orange - graphics.fillOval(0, 0, size, size) - graphics.dispose() - return image +object TrayIcon : Painter() { + override val intrinsicSize = Size(256f, 256f) + + override fun DrawScope.onDraw() { + drawOval(Color(0xFFFFA500)) + } } ``` ![](hide_instead_of_close.gif) @@ -195,11 +186,11 @@ import androidx.compose.runtime.key import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.remember import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.window.ApplicationScope import androidx.compose.ui.window.MenuBar import androidx.compose.ui.window.Window import androidx.compose.ui.window.application -@OptIn(ExperimentalComposeUiApi::class) fun main() = application { val applicationState = remember { MyApplicationState() } @@ -212,7 +203,7 @@ fun main() = application { @OptIn(ExperimentalComposeUiApi::class) @Composable -private fun MyWindow( +private fun ApplicationScope.MyWindow( state: MyWindowState ) = Window(onCloseRequest = state::close, title = state.title) { MenuBar { @@ -271,7 +262,6 @@ import androidx.compose.foundation.layout.Row import androidx.compose.material.Checkbox import androidx.compose.material.Text import androidx.compose.ui.Alignment -import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Window @@ -280,7 +270,6 @@ import androidx.compose.ui.window.WindowPosition import androidx.compose.ui.window.application import androidx.compose.ui.window.rememberWindowState -@OptIn(ExperimentalComposeUiApi::class) fun main() = application { val state = rememberWindowState(placement = WindowPlacement.Maximized) @@ -344,10 +333,9 @@ fun main() = application { ## Listening the state of the window Reading the state in composition is useful when you need to update UI, but there are cases when you need to react to the state changes and send a value to another non-composable level of your application (write it to the database, for example): -``` +```kotlin import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.snapshotFlow -import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.window.Window import androidx.compose.ui.window.WindowPosition import androidx.compose.ui.window.WindowSize @@ -357,20 +345,19 @@ import kotlinx.coroutines.flow.filterNot import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach -@OptIn(ExperimentalComposeUiApi::class) fun main() = application { val state = rememberWindowState() - Window(state) { + Window(onCloseRequest = ::exitApplication, state) { // Content - + LaunchedEffect(state) { snapshotFlow { state.size } .onEach(::onWindowResize) .launchIn(this) snapshotFlow { state.position } - .filterNot { it.isInitial } + .filterNot { it.isSpecified } .onEach(::onWindowRelocate) .launchIn(this) } @@ -386,35 +373,6 @@ private fun onWindowRelocate(position: WindowPosition) { } ``` -## Handle window-level shortcuts -```kotlin -import androidx.compose.material.TextField -import androidx.compose.ui.ExperimentalComposeUiApi -import androidx.compose.ui.input.key.Key -import androidx.compose.ui.input.key.KeyEventType -import androidx.compose.ui.input.key.key -import androidx.compose.ui.input.key.type -import androidx.compose.ui.window.Window -import androidx.compose.ui.window.application - -@OptIn(ExperimentalComposeUiApi::class) -fun main() = application { - Window( - onCloseRequest = ::exitApplication, - onPreviewKeyEvent = { - if (it.type == KeyEventType.KeyDown && it.key == Key.Escape) { - exitApplication() - true - } else { - false - } - } - ) { - TextField("Text", {}) - } -} -``` - ## Dialogs There are two types of window – modal and regular. Below are the functions for creating each: @@ -427,19 +385,16 @@ You can see an example of both types of window below. import androidx.compose.material.Button import androidx.compose.material.Text import androidx.compose.runtime.getValue -import androidx.compose.runtime.setValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment -import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogState import androidx.compose.ui.window.Window import androidx.compose.ui.window.WindowPosition -import androidx.compose.ui.window.WindowState import androidx.compose.ui.window.application -@OptIn(ExperimentalComposeUiApi::class) fun main() = application { Window( onCloseRequest = ::exitApplication, @@ -465,13 +420,11 @@ fun main() = application { ## Swing interoperability Because Compose for Desktop uses Swing under the hood, it is possible to create a window using Swing directly: ```kotlin -import androidx.compose.desktop.ComposeWindow -import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.awt.ComposeWindow import java.awt.Dimension import javax.swing.JFrame import javax.swing.SwingUtilities -@OptIn(ExperimentalComposeUiApi::class) fun main() = SwingUtilities.invokeLater { ComposeWindow().apply { size = Dimension(300, 300) @@ -509,13 +462,11 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.window.AwtWindow import androidx.compose.ui.window.application import java.awt.FileDialog import java.awt.Frame -@OptIn(ExperimentalComposeUiApi::class) fun main() = application { var isOpen by remember { mutableStateOf(true) } @@ -529,7 +480,6 @@ fun main() = application { } } -@OptIn(ExperimentalComposeUiApi::class) @Composable private fun FileDialog( parent: Frame? = null, @@ -547,4 +497,4 @@ private fun FileDialog( }, dispose = FileDialog::dispose ) -``` +``` \ No newline at end of file