@ -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) |
After Width: | Height: | Size: 798 KiB |
After Width: | Height: | Size: 3.2 MiB |
@ -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 |
||||
) |
||||
``` |
After Width: | Height: | Size: 538 KiB |
After Width: | Height: | Size: 904 KiB |
After Width: | Height: | Size: 2.8 MiB |
After Width: | Height: | Size: 3.2 MiB |
After Width: | Height: | Size: 308 KiB |
After Width: | Height: | Size: 568 KiB |