# Context Menu in Compose for Desktop ## What is covered In this tutorial we will cover all aspects of work with Context Menu using the Compose UI framework. ## Default context menu There is out-of-the box context menu support for TextField and Selectable text. To enable standard context menu for a TextField you just need to put it inside DesktopMaterialTheme: ```kotlin import androidx.compose.material.Text import androidx.compose.material.TextField import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.window.singleWindowApplication fun main() = singleWindowApplication(title = "Context menu") { val text = remember { mutableStateOf("Hello!") } TextField( value = text.value, onValueChange = { text.value = it }, label = { Text(text = "Input") } ) } ``` image Standard context menu for TextField contains the following items based on text selection: Copy, Cut, Paste, Select All. Enabling standard context menu for a Text component is similar - you just need to make it selectable: ```kotlin import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.material.Text import androidx.compose.ui.window.singleWindowApplication fun main() = singleWindowApplication(title = "Context menu") { SelectionContainer { Text("Hello World!") } } ``` 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: ```kotlin import androidx.compose.foundation.ContextMenuDataProvider import androidx.compose.foundation.ContextMenuItem import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.material.Text import androidx.compose.material.TextField import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.compose.ui.window.singleWindowApplication fun main() = singleWindowApplication(title = "Context menu") { val text = remember { mutableStateOf("Hello!") } Column { ContextMenuDataProvider( items = { listOf( ContextMenuItem("User-defined Action") {/*do something here*/ }, ContextMenuItem("Another user-defined action") {/*do something else*/ } ) } ) { TextField( value = text.value, onValueChange = { text.value = it }, label = { Text(text = "Input") } ) Spacer(Modifier.height(16.dp)) SelectionContainer { Text("Hello World!") } } } } ``` 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 similar to ContextMenuDataProvider. ```kotlin import androidx.compose.foundation.ContextMenuArea import androidx.compose.foundation.ContextMenuItem 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.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import androidx.compose.ui.window.singleWindowApplication fun main() = singleWindowApplication(title = "Context menu") { ContextMenuArea(items = { listOf( ContextMenuItem("User-defined Action") {/*do something here*/}, ContextMenuItem("Another user-defined action") {/*do something else*/} ) }) { Box(modifier = Modifier.background(Color.Blue).height(100.dp).width(100.dp)) } } ``` 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