diff --git a/.gitignore b/.gitignore index c2b13ed18a..0d380d087a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +.DS_Store + # Compiled class file *.class diff --git a/tutorials/Tray_Notifications_MenuBar/app_menubar.gif b/tutorials/Tray_Notifications_MenuBar/app_menubar.gif new file mode 100644 index 0000000000..6f186df333 Binary files /dev/null and b/tutorials/Tray_Notifications_MenuBar/app_menubar.gif differ diff --git a/tutorials/Tray_Notifications_MenuBar/main.md b/tutorials/Tray_Notifications_MenuBar/main.md new file mode 100755 index 0000000000..a98cd6712a --- /dev/null +++ b/tutorials/Tray_Notifications_MenuBar/main.md @@ -0,0 +1,304 @@ +# Tray and menu notification + +## What is covered + +In this guide, we'll show you how to work with system tray, create application menu bar and create window specific menu bar, and send system notifications using Compose for Desktop. + +## Tray + +You can add an application icon into the system tray. Using Tray, you can also send notifications to the user. There are 3 types of notifications: + +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.Text +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.onActive +import androidx.compose.runtime.onDispose +import androidx.compose.ui.Alignment +import androidx.compose.ui.window.MenuItem +import androidx.compose.ui.window.Tray +import androidx.compose.ui.Modifier +import java.awt.Color +import java.awt.Graphics2D +import java.awt.image.BufferedImage + +fun main() { + val count = mutableStateOf(0) + Window( + icon = getMyAppIcon() + ) { + onActive { + 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(), + alignment = 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 notifications: + +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.foundation.Text +import androidx.compose.material.Button +import androidx.compose.ui.window.Notifier +import java.awt.Color +import java.awt.Graphics2D +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 any particular window. +To create a common context menu for all application windows, you need to configure the AppManager. + +```kotlin +import androidx.compose.desktop.AppManager +import androidx.compose.desktop.Window +import androidx.compose.foundation.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.input.key.Key +import androidx.compose.ui.Modifier +import androidx.compose.ui.window.KeyStroke +import androidx.compose.ui.window.MenuItem +import androidx.compose.ui.window.Menu +import androidx.compose.ui.window.MenuBar + +fun main() { + 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(), + alignment = Alignment.Center + ) { + Text(text = action.value) + } + } +} +``` + +![Application MenuBar](app_menubar.gif) + +You can to create a MenuBar for a specific window (while others will use the common MenuBar, if defined). + +```kotlin +import androidx.compose.desktop.AppManager +import androidx.compose.desktop.Window +import androidx.compose.foundation.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.input.key.Key +import androidx.compose.ui.Modifier +import androidx.compose.ui.window.KeyStroke +import androidx.compose.ui.window.MenuItem +import androidx.compose.ui.window.Menu +import androidx.compose.ui.window.MenuBar +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.IntSize + +fun main() { + 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(), + alignment = Alignment.Center + ) { + Text(text = action.value) + } + } +} +``` + +![Window MenuBar](window_menubar.gif) diff --git a/tutorials/Tray_Notifications_MenuBar/notifier.gif b/tutorials/Tray_Notifications_MenuBar/notifier.gif new file mode 100644 index 0000000000..9ab6638f77 Binary files /dev/null and b/tutorials/Tray_Notifications_MenuBar/notifier.gif differ diff --git a/tutorials/Tray_Notifications_MenuBar/tray.gif b/tutorials/Tray_Notifications_MenuBar/tray.gif new file mode 100644 index 0000000000..4f42bcb2a1 Binary files /dev/null and b/tutorials/Tray_Notifications_MenuBar/tray.gif differ diff --git a/tutorials/Tray_Notifications_MenuBar/window_menubar.gif b/tutorials/Tray_Notifications_MenuBar/window_menubar.gif new file mode 100644 index 0000000000..8c08c60e28 Binary files /dev/null and b/tutorials/Tray_Notifications_MenuBar/window_menubar.gif differ diff --git a/tutorials/Window_API/center_the_window.gif b/tutorials/Window_API/center_the_window.gif new file mode 100644 index 0000000000..9b8eb0aeda Binary files /dev/null and b/tutorials/Window_API/center_the_window.gif differ diff --git a/tutorials/Window_API/current_window.gif b/tutorials/Window_API/current_window.gif new file mode 100644 index 0000000000..dc0807761b Binary files /dev/null and b/tutorials/Window_API/current_window.gif differ diff --git a/tutorials/Window_API/focus_the_window.gif b/tutorials/Window_API/focus_the_window.gif new file mode 100644 index 0000000000..2d62993bb4 Binary files /dev/null and b/tutorials/Window_API/focus_the_window.gif differ diff --git a/tutorials/Window_API/main.md b/tutorials/Window_API/main.md new file mode 100755 index 0000000000..a6f32ce5e5 --- /dev/null +++ b/tutorials/Window_API/main.md @@ -0,0 +1,422 @@ +# OS windows management + +## What is covered + +In this guide, we'll 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 + +fun main() { + AppWindow().show { + // content + } +} +``` + +There are two types of windows - modal and regular. Below are 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 for both types of windows below. + +```kotlin +import androidx.compose.desktop.Window +import androidx.compose.foundation.Text +import androidx.compose.material.Button +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.window.Dialog + +fun main() { + Window { + val dialogState = remember { mutableStateOf(false) } + + Button(onClick = { dialogState.value = true }) { + Text(text = "Open dialog") + } + + if (dialogState.value) { + Dialog( + onDismissEvent = { dialogState.value = false } + ) { + // dialog's content + } + } + } +} +``` + +## Window attributes + +Each window has 9 parameters listed below, 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. events - window events +9. onDismissEvent - event when removing the window content from a composition + +An example of using window parameters at the creation step: + +```kotlin +import androidx.compose.desktop.AppManager +import androidx.compose.desktop.Window +import androidx.compose.desktop.WindowEvents +import androidx.compose.foundation.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.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.MenuItem +import androidx.compose.ui.window.KeyStroke +import androidx.compose.ui.window.Menu +import androidx.compose.ui.window.MenuBar +import java.awt.Color +import java.awt.Graphics2D +import java.awt.image.BufferedImage + +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(), + alignment = 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.setColor(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. icon - window icon image +7. 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.AppWindowAmbient +import androidx.compose.desktop.Window +import androidx.compose.foundation.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 { + val current = AppWindowAmbient.current + + // content + Box( + modifier = Modifier.fillMaxSize(), + alignment = Alignment.Center + ) { + Column { + Text(text = "Location: ${windowPos.value}") + Button( + onClick = { + if (current != null) { + 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.foundation.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(), + alignment = 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, one can change the properties of 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 + +```kotlin +import androidx.compose.desktop.AppWindowAmbient +import androidx.compose.desktop.Window +import androidx.compose.foundation.Text +import androidx.compose.material.Button + +fun main() { + Window { + val current = AppWindowAmbient.current + + // content + Button( + onClick = { + if (current != null) { + current.setWindowCentered() + } + } + ) { + Text(text = "Center the window") + } + } +} +``` + +![Window properties](center_the_window.gif) + +## Window events + +Events could be defined using the events parameter at 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.foundation.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.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(), + alignment = 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 on the level of top level windows. For more detailed customization, you can access the JFrame (Swing window representation): + +```kotlin +import androidx.compose.desktop.AppManager +import androidx.compose.desktop.Window +import androidx.compose.foundation.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(), + alignment = 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/scaling_factor.jpg b/tutorials/Window_API/scaling_factor.jpg new file mode 100644 index 0000000000..6faa9eda03 Binary files /dev/null and b/tutorials/Window_API/scaling_factor.jpg differ diff --git a/tutorials/Window_API/window_attr.gif b/tutorials/Window_API/window_attr.gif new file mode 100644 index 0000000000..a0d59d1c25 Binary files /dev/null and b/tutorials/Window_API/window_attr.gif differ