Browse Source

Tutorials for new Composable Windows API (#721)

pull/735/merge
Igor Demin 4 years ago committed by GitHub
parent
commit
d8d693c09d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      README.md
  2. 2
      tutorials/Tray_Notifications_MenuBar/README.md
  3. 176
      tutorials/Tray_Notifications_MenuBar_new/README.md
  4. BIN
      tutorials/Tray_Notifications_MenuBar_new/tray.gif
  5. BIN
      tutorials/Tray_Notifications_MenuBar_new/window_menubar.gif
  6. 2
      tutorials/Window_API/README.md
  7. 494
      tutorials/Window_API_new/README.md
  8. BIN
      tutorials/Window_API_new/ask_to_close.gif
  9. BIN
      tutorials/Window_API_new/hide_instead_of_close.gif
  10. BIN
      tutorials/Window_API_new/multiple_windows.gif
  11. BIN
      tutorials/Window_API_new/state.gif
  12. BIN
      tutorials/Window_API_new/window_properties.gif
  13. BIN
      tutorials/Window_API_new/window_splash.gif

4
README.md

@ -31,11 +31,13 @@ at https://android.googlesource.com/platform/frameworks/support.
* [Mouse events and hover](tutorials/Mouse_Events) * [Mouse events and hover](tutorials/Mouse_Events)
* [Scrolling and scrollbars](tutorials/Desktop_Components) * [Scrolling and scrollbars](tutorials/Desktop_Components)
* [Tooltips](tutorials/Desktop_Components#tooltips) * [Tooltips](tutorials/Desktop_Components#tooltips)
* [Top level windows management](tutorials/Window_API)
* [Top level windows management (new Composable API, experimental)](tutorials/Window_API_new)
* [Menu, tray, notifications](tutorials/Tray_Notifications_MenuBar) * [Menu, tray, notifications](tutorials/Tray_Notifications_MenuBar)
* [Menu, tray, notifications (new Composable API, experimental)](tutorials/Tray_Notifications_MenuBar_new)
* [Keyboard support](tutorials/Keyboard) * [Keyboard support](tutorials/Keyboard)
* [Building native distribution](tutorials/Native_distributions_and_local_execution) * [Building native distribution](tutorials/Native_distributions_and_local_execution)
* [Signing and notarization](tutorials/Signing_and_notarization_on_macOS) * [Signing and notarization](tutorials/Signing_and_notarization_on_macOS)
* [Window control API](tutorials/Window_API)
* [Swing interoperability](tutorials/Swing_Integration) * [Swing interoperability](tutorials/Swing_Integration)
* [Navigation](tutorials/Navigation) * [Navigation](tutorials/Navigation)
* [components](components) - custom components of Compose for Desktop * [components](components) - custom components of Compose for Desktop

2
tutorials/Tray_Notifications_MenuBar/README.md

@ -1,4 +1,4 @@
# Tray and menu notification # Menu, tray, notifications
## What is covered ## What is covered

176
tutorials/Tray_Notifications_MenuBar_new/README.md

@ -0,0 +1,176 @@
# Menu, tray, notifications (new Composable API, experimental)
## What is covered
In this tutorial we'll show you how to work with the system tray, send system notifications, and create a menu bar 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.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.Text
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.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) }
if (isOpen) {
Window(
icon = remember { getMyAppIcon() }
) {
val trayState = rememberTrayState()
val notification = Notification("Notification", "Message from MyApp!")
Tray(
state = trayState,
icon = remember { getTrayIcon() },
menu = {
Item(
"Increment value",
onClick = {
count++
}
)
Item(
"Send notification",
onClick = {
trayState.sendNotification(notification)
}
)
Item(
"Exit",
onClick = {
isOpen = false
}
)
}
)
// content
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
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
}
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
}
```
![](tray.gif)
## MenuBar
MenuBar is used to create and customize the menu bar for a particular window.
```kotlin
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.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
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 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 {
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" })
}
}
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" },)
}
}
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text(text = action)
}
}
}
}
}
```
![](window_menubar.gif)

BIN
tutorials/Tray_Notifications_MenuBar_new/tray.gif

Binary file not shown.

After

Width:  |  Height:  |  Size: 798 KiB

BIN
tutorials/Tray_Notifications_MenuBar_new/window_menubar.gif

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 MiB

2
tutorials/Window_API/README.md

@ -1,4 +1,4 @@
# OS windows management # Top level windows management
## What is covered ## What is covered

494
tutorials/Window_API_new/README.md

@ -0,0 +1,494 @@
# Top level windows management (new Composable API, experimental)
## What is covered
In this tutorial we will show you how to work with windows using Compose for Desktop.
We represent window state in the shape suitable for Compose-style state manipulations and automatically mapping it into the operating system window state.
So top level windows can be both conditionally created in other composable functions and their window manager state could be manipulated using state produced by `rememberWindowState()` function.
## Open and close windows
The main function for creating windows is `Window`. This function should be used in Composable scope. The easiest way to create a Composable scope is to use `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 {
// Content
}
}
```
`Window` is Composable function. It means you can change its properties in a declarative way:
```kotlin
import androidx.compose.material.Button
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.Window
import androidx.compose.ui.window.application
@OptIn(ExperimentalComposeUiApi::class)
fun main() = application {
var fileName by remember { mutableStateOf("Untitled") }
Window(title = "$fileName - Editor") {
Button(onClick = { fileName = "note.txt" }) {
Text("Save")
}
}
}
```
![](window_properties.gif)
You can also close/open windows using a simple `if` statement.
When Window leaves the composition (isPerformingTask becomes `false`) - the native window automatically closes.
```kotlin
import androidx.compose.material.Text
import androidx.compose.runtime.LaunchedEffect
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 {
Text("Performing some tasks. Please wait!")
}
} else {
Window {
Text("Hello, World!")
}
}
}
```
![](window_splash.gif)
If window requires some custom logic on close (for example, to show a dialog), you can override close action using `onCloseRequest`.
See that instead of an imperative approach to closing the window (`window.close()`) we use a declarative - close the window in response of changing the state: `isOpen = false`.
```kotlin
import androidx.compose.material.Button
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.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 }
) {
if (isAskingToClose) {
Dialog(
title = "Close the document without saving?",
onCloseRequest = { isAskingToClose = false }
) {
Button(
onClick = { isOpen = false }
) {
Text("Yes")
}
}
}
}
}
}
```
![](ask_to_close.gif)
If you don't need to close the window on close button and just need to hide it (for example to tray), you can change `windowState.isVisible` state:
```kotlin
import androidx.compose.material.Text
import androidx.compose.runtime.LaunchedEffect
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.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 {
val state = rememberWindowState()
Window(
state,
title = "Counter",
onCloseRequest = { state.isVisible = false }
) {
var counter by remember { mutableStateOf(0) }
LaunchedEffect(Unit) {
while (true) {
counter++
delay(1000)
}
}
Text(counter.toString())
}
if (!state.isVisible && state.isOpen) {
Tray(
remember { getTrayIcon() },
hint = "Counter",
onAction = { state.isVisible = true },
menu = {
Item("Exit", onClick = { state.isOpen = false })
},
)
}
}
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
}
```
![](hide_instead_of_close.gif)
If application has multiple windows then it is better to hoist its state into a separate class and open/close window in response of some list state changes (see [notepad example](https://github.com/JetBrains/compose-jb/tree/master/examples/notepad) for more complex use cases):
```kotlin
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.remember
import androidx.compose.ui.ExperimentalComposeUiApi
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() }
for (window in applicationState.windows) {
MyWindow(window)
}
}
@OptIn(ExperimentalComposeUiApi::class)
@Composable
private fun MyWindow(
state: MyWindowState
) = Window(title = state.title, onCloseRequest = state::close) {
MenuBar {
Menu("File") {
Item("New window", onClick = state.openNewWindow)
Item("Exit", onClick = state.exit)
}
}
}
private class MyApplicationState {
val windows = mutableStateListOf<MyWindowState>()
init {
windows += MyWindowState("Initial window")
}
fun openNewWindow() {
windows += MyWindowState("Window ${windows.size}")
}
fun exit() {
windows.clear()
}
private fun MyWindowState(
title: String
) = MyWindowState(
title,
openNewWindow = ::openNewWindow,
exit = ::exit,
windows::remove
)
}
private class MyWindowState(
val title: String,
val openNewWindow: () -> Unit,
val exit: () -> Unit,
private val close: (MyWindowState) -> Unit
) {
fun close() = close(this)
}
```
![](multiple_windows.gif)
## Changing state (maximized, minimized, fullscreen, size, position) of the window.
Some state of the native window is hoisted into a separate API class `WindowState`. You can change its properties in callbacks or observe it in Composable's:
```kotlin
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
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
import androidx.compose.ui.window.application
import androidx.compose.ui.window.rememberWindowState
@OptIn(ExperimentalComposeUiApi::class)
fun main() = application {
val state = rememberWindowState(isMaximized = true)
Window(state) {
Column {
Row(verticalAlignment = Alignment.CenterVertically) {
Checkbox(state.isFullscreen, { state.isFullscreen = !state.isFullscreen })
Text("isFullscreen")
}
Row(verticalAlignment = Alignment.CenterVertically) {
Checkbox(state.isMaximized, { state.isMaximized = !state.isMaximized })
Text("isMaximized")
}
Row(verticalAlignment = Alignment.CenterVertically) {
Checkbox(state.isMinimized, { state.isMinimized = !state.isMinimized })
Text("isMinimized")
}
Text(
"Position ${state.position}",
Modifier.clickable {
state.position = state.position.copy(x = state.position.x + 10.dp)
}
)
Text(
"Size ${state.size}",
Modifier.clickable {
state.size = state.size.copy(width = state.size.width + 10.dp)
}
)
}
}
}
```
![](state.gif)
## Handle window-level shortcuts
```kotlin
import androidx.compose.foundation.layout.Box
import androidx.compose.material.TextField
import androidx.compose.runtime.LaunchedEffect
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.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.focusTarget
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.onPreviewKeyEvent
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
@OptIn(ExperimentalComposeUiApi::class)
fun main() = application {
var isOpen by remember { mutableStateOf(true) }
if (isOpen) {
Window {
val focusRequester = remember(::FocusRequester)
LaunchedEffect(Unit) {
focusRequester.requestFocus()
}
Box(
Modifier
.focusRequester(focusRequester)
.focusTarget()
.onPreviewKeyEvent {
when (it.key) {
Key.Escape -> {
isOpen = false
true
}
else -> false
}
}
) {
TextField("Text", {})
}
}
}
}
```
(currently it is a bit verbose; in the future we will investigate - how can we provide a simple API for handling window key events).
## Dialogs
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.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.ui.Alignment
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 {
Window {
var isDialogOpen by remember { mutableStateOf(false) }
Button(onClick = { isDialogOpen = true }) {
Text(text = "Open dialog")
}
if (isDialogOpen) {
Dialog(
initialAlignment = Alignment.Center,
onCloseRequest = { isDialogOpen = false }
) {
// Dialog's content
}
}
}
}
```
## 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 java.awt.Dimension
import javax.swing.JFrame
import javax.swing.SwingUtilities
@OptIn(ExperimentalComposeUiApi::class)
fun main() = SwingUtilities.invokeLater {
ComposeWindow().apply {
size = Dimension(300, 300)
defaultCloseOperation = JFrame.DISPOSE_ON_CLOSE
setContent {
// Content
}
isVisible = true
}
}
```
You can also access ComposeWindow in the Composable `Window` scope:
```kotlin
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
import java.awt.Cursor
@OptIn(ExperimentalComposeUiApi::class)
fun main() = application {
Window {
LaunchedEffect(Unit) {
window.cursor = Cursor(Cursor.CROSSHAIR_CURSOR)
}
}
}
```
If you need a dialog that is implemented in Swing, you can wrap it into Composable function:
```kotlin
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.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) }
if (isOpen) {
FileDialog(
onCloseRequest = {
isOpen = false
println("Result $it")
}
)
}
}
@OptIn(ExperimentalComposeUiApi::class)
@Composable
private fun FileDialog(
parent: Frame? = null,
onCloseRequest: (result: String?) -> Unit
) = AwtWindow(
create = {
object : FileDialog(parent, "Choose a file", LOAD) {
override fun setVisible(value: Boolean) {
super.setVisible(value)
if (value) {
onCloseRequest(file)
}
}
}
},
dispose = FileDialog::dispose
)
```

BIN
tutorials/Window_API_new/ask_to_close.gif

Binary file not shown.

After

Width:  |  Height:  |  Size: 538 KiB

BIN
tutorials/Window_API_new/hide_instead_of_close.gif

Binary file not shown.

After

Width:  |  Height:  |  Size: 904 KiB

BIN
tutorials/Window_API_new/multiple_windows.gif

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

BIN
tutorials/Window_API_new/state.gif

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 MiB

BIN
tutorials/Window_API_new/window_properties.gif

Binary file not shown.

After

Width:  |  Height:  |  Size: 308 KiB

BIN
tutorials/Window_API_new/window_splash.gif

Binary file not shown.

After

Width:  |  Height:  |  Size: 568 KiB

Loading…
Cancel
Save