diff --git a/tutorials/Context_Menu/README.md b/tutorials/Context_Menu/README.md index 645685810a..e406e18055 100644 --- a/tutorials/Context_Menu/README.md +++ b/tutorials/Context_Menu/README.md @@ -15,10 +15,8 @@ import androidx.compose.material.Text import androidx.compose.material.TextField import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.window.singleWindowApplication -@OptIn(ExperimentalComposeUiApi::class, androidx.compose.foundation.ExperimentalFoundationApi::class) fun main() = singleWindowApplication(title = "Context menu") { val text = remember { mutableStateOf("Hello!") } TextField( @@ -28,6 +26,8 @@ fun main() = singleWindowApplication(title = "Context menu") { ) } ``` +image + Standard context menu for TextField contains the following items based on text selection: Copy, Cut, Paste, Select All. @@ -36,17 +36,17 @@ Enabling standard context menu for a Text component is similar - you just need t ```kotlin import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.material.Text -import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.window.singleWindowApplication -@OptIn(ExperimentalComposeUiApi::class, androidx.compose.foundation.ExperimentalFoundationApi::class) fun main() = singleWindowApplication(title = "Context menu") { SelectionContainer { Text("Hello World!") } } ``` -Context menu for text contains just Copy action. +Context menu for text contains just Copy action: + +image ## User-defined context menu To enable additional context menu items for TextField and Text components, ContextMenuDataProvider and ContextMenuItem elements are used: @@ -62,12 +62,10 @@ import androidx.compose.material.Text import androidx.compose.material.TextField import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.compose.ui.window.singleWindowApplication -@OptIn(ExperimentalComposeUiApi::class, androidx.compose.foundation.ExperimentalFoundationApi::class) fun main() = singleWindowApplication(title = "Context menu") { val text = remember { mutableStateOf("Hello!") } Column { @@ -94,7 +92,9 @@ fun main() = singleWindowApplication(title = "Context menu") { } } ``` -In this example Text/TextField context menus will be extended with two additional items. +In this example Text/TextField context menus will be extended with two additional items: + +image ## Context menu for an arbitrary area There is a possibility to create a context menu for an arbitrary application window area. This is implemented using ContextMenuArea API that is @@ -106,13 +106,11 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.width -import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import androidx.compose.ui.window.singleWindowApplication -@OptIn(ExperimentalComposeUiApi::class, androidx.compose.foundation.ExperimentalFoundationApi::class) fun main() = singleWindowApplication(title = "Context menu") { ContextMenuArea(items = { listOf( @@ -124,4 +122,264 @@ fun main() = singleWindowApplication(title = "Context menu") { } } ``` -Right click on the Blue Square will show a context menu with two items +Right click on the Blue Square will show a context menu with two items: + +image + +## Styling context menu +Style of context menu doesn't comply MaterialTheme. To change its colors, you should override `LocalContextMenuRepresentation`: +```kotlin +import androidx.compose.foundation.DarkDefaultContextMenuRepresentation +import androidx.compose.foundation.LightDefaultContextMenuRepresentation +import androidx.compose.foundation.LocalContextMenuRepresentation +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Surface +import androidx.compose.material.TextField +import androidx.compose.material.darkColors +import androidx.compose.material.lightColors +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.window.singleWindowApplication + +fun main() = singleWindowApplication { + isSystemInDarkTheme() + MaterialTheme( + colors = if (isSystemInDarkTheme()) darkColors() else lightColors() + ) { + val contextMenuRepresentation = if (isSystemInDarkTheme()) { + DarkDefaultContextMenuRepresentation + } else { + LightDefaultContextMenuRepresentation + } + CompositionLocalProvider(LocalContextMenuRepresentation provides contextMenuRepresentation) { + Surface(Modifier.fillMaxSize()) { + Box { + var value by remember { mutableStateOf("") } + TextField(value, { value = it }) + } + } + } + } +} +``` +image + +## Custom text context menu +You can override text menu for all texts and text fields in your application, overriding `TextContextMenu`: +```kotlin +import androidx.compose.foundation.ContextMenuDataProvider +import androidx.compose.foundation.ContextMenuItem +import androidx.compose.foundation.ContextMenuState +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.text.LocalTextContextMenu +import androidx.compose.foundation.text.TextContextMenu +import androidx.compose.foundation.text.selection.SelectionContainer +import androidx.compose.material.Text +import androidx.compose.material.TextField +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.awt.ComposePanel +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.text.AnnotatedString +import java.awt.Dimension +import java.net.URLEncoder +import java.nio.charset.Charset +import javax.swing.JFrame +import javax.swing.SwingUtilities + +fun main() = SwingUtilities.invokeLater { + val panel = ComposePanel() + panel.setContent { + CustomTextMenuProvider { + Column { + SelectionContainer { + Text("Hello, Compose!") + } + + var text by remember { mutableStateOf("") } + + TextField(text, { text = it }) + } + } + } + + val window = JFrame() + window.contentPane.add(panel) + window.size = Dimension(800, 600) + window.isVisible = true +} + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun CustomTextMenuProvider(content: @Composable () -> Unit) { + val textMenu = LocalTextContextMenu.current + val uriHandler = LocalUriHandler.current + CompositionLocalProvider( + LocalTextContextMenu provides object : TextContextMenu { + @Composable + override fun Area( + textManager: TextContextMenu.TextManager, + state: ContextMenuState, + content: @Composable () -> Unit + ) { + // Here we reuse the original TextContextMenu, but add an additional item to item on the bottom. + ContextMenuDataProvider({ + val shortText = textManager.selectedText.crop() + if (shortText.isNotEmpty()) { + val encoded = URLEncoder.encode(shortText, Charset.defaultCharset()) + listOf(ContextMenuItem("Search $shortText") { + uriHandler.openUri("https://google.com/search?q=$encoded") + }) + } else { + emptyList() + } + }) { + textMenu.Area(textManager, state, content = content) + } + } + }, + content = content + ) +} + +private fun AnnotatedString.crop() = if (length <= 5) toString() else "${take(5)}..." +``` +image + +## Swing interoperability +If you are embedding Compose into an existing application, you may want the text context menu to look the same as in other parts of the application. To do this, there is `JPopupTextMenu`: +```kotlin +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.text.JPopupTextMenu +import androidx.compose.foundation.text.LocalTextContextMenu +import androidx.compose.foundation.text.selection.SelectionContainer +import androidx.compose.material.Text +import androidx.compose.material.TextField +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.awt.ComposePanel +import androidx.compose.ui.platform.LocalLocalization +import java.awt.Color +import java.awt.Component +import java.awt.Dimension +import java.awt.Graphics +import java.awt.event.KeyEvent +import java.awt.event.KeyEvent.CTRL_DOWN_MASK +import java.awt.event.KeyEvent.META_DOWN_MASK +import javax.swing.Icon +import javax.swing.JFrame +import javax.swing.JMenuItem +import javax.swing.JPopupMenu +import javax.swing.KeyStroke.getKeyStroke +import javax.swing.SwingUtilities +import org.jetbrains.skiko.hostOs + +fun main() = SwingUtilities.invokeLater { + val panel = ComposePanel() + panel.setContent { + JPopupTextMenuProvider(panel) { + Column { + SelectionContainer { + Text("Hello, Compose!") + } + + var text by remember { mutableStateOf("") } + + TextField(text, { text = it }) + } + } + } + + val window = JFrame() + window.contentPane.add(panel) + window.size = Dimension(800, 600) + window.isVisible = true +} + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun JPopupTextMenuProvider(owner: Component, content: @Composable () -> Unit) { + val localization = LocalLocalization.current + CompositionLocalProvider( + LocalTextContextMenu provides JPopupTextMenu(owner) { textManager, items -> + JPopupMenu().apply { + textManager.cut?.also { + add( + swingItem(localization.cut, Color.RED, KeyEvent.VK_X, it) + ) + } + textManager.copy?.also { + add( + swingItem(localization.copy, Color.GREEN, KeyEvent.VK_C, it) + ) + } + textManager.paste?.also { + add( + swingItem(localization.paste, Color.BLUE, KeyEvent.VK_V, it) + ) + } + textManager.selectAll?.also { + add(JPopupMenu.Separator()) + add( + swingItem(localization.selectAll, Color.BLACK, KeyEvent.VK_A, it) + ) + } + + // Here we add other items that can be defined additionaly in the other places of the application via ContextMenuDataProvider + for (item in items) { + add( + JMenuItem(item.label).apply { + addActionListener { item.onClick() } + } + ) + } + } + }, + content = content + ) +} + +private fun swingItem( + label: String, + color: Color, + key: Int, + onClick: () -> Unit +) = JMenuItem(label).apply { + icon = circleIcon(color) + accelerator = getKeyStroke(key, if (hostOs.isMacOS) META_DOWN_MASK else CTRL_DOWN_MASK) + addActionListener { onClick() } +} + +private fun circleIcon(color: Color) = object : Icon { + override fun paintIcon(c: Component?, g: Graphics, x: Int, y: Int) { + g.create().apply { + this.color = color + translate(16, 2) + fillOval(0, 0, 16, 16) + } + } + + override fun getIconWidth() = 16 + + override fun getIconHeight() = 16 +} +``` +image +