Compose Multiplatform для настольных компьютеров из коробки поддерживает контекстные меню для текста и позволяет удобно настраивать любые контекстные меню: добавлять элементы, задавать темы и изменять текст.
Контекстное меню в произвольной области
Вы можете создать контекстное меню для любой области приложения. Используйте ContextMenuArea, чтобы определить контейнер, в котором щелчок правой кнопкой мыши будет открывать контекстное меню:
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") { // Custom action }, ContextMenuItem("Another user-defined action") { // Another custom action } ) }) { // Blue box where context menu will be available Box(modifier = Modifier.background(Color.Blue).height(100.dp).width(100.dp)) } }
Настройка темы
Вы можете настроить цвета контекстного меню, чтобы создать адаптивный пользовательский интерфейс, соответствующий настройкам системы, и избежать резких изменений контрастности при переключении между приложениями. Для светлой и тёмной тем по умолчанию предусмотрены две встроенные реализации: LightDefaultContextMenuRepresentation и DarkDefaultContextMenuRepresentation. Они не применяются к цветам контекстного меню автоматически, поэтому нужно задать подходящую тему с помощью LocalContextMenuRepresentation:
Compose Multiplatform для настольных компьютеров предлагает встроенные контекстные меню для TextField и выделяемых Text.
Контекстное меню текстового поля по умолчанию включает следующие действия, зависящие от положения курсора и диапазона выделения: копировать, вырезать, вставить и выделить всё. Это стандартное контекстное меню доступно по умолчанию в компоненте material TextField (androidx.compose.material.TextField или androidx.compose.material3.TextField) и компоненте foundation BasicTextField (androidx.compose.foundation.text.BasicTextField).
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") } ) }
Контекстное меню простого текстового элемента включает только действие копирования. Чтобы включить контекстное меню для компонента Text, сделайте текст выделяемым, обернув его в SelectionContainer:
Чтобы добавить пользовательские действия в контекстное меню компонентов TextField и Text, задайте новые элементы с помощью ContextMenuItem и добавьте их в иерархию элементов контекстного меню через ContextMenuDataProvider. Например, в следующем фрагменте кода показано, как добавить два новых пользовательских действия в контекстные меню текстового поля и простого выделяемого текстового элемента:
Переопределение контекстного меню для текста по умолчанию
Чтобы переопределить контекстное меню по умолчанию для текстовых полей и выделяемых текстовых элементов, переопределите интерфейс TextContextMenu. В следующем фрагменте кода мы используем исходный TextContextMenu, добавляя в конец списка ещё один элемент. Новый элемент адаптируется к выделенному тексту:
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.platform.LocalUriHandler import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.window.singleWindowApplication import java.net.URLEncoder import java.nio.charset.Charset fun main() = singleWindowApplication(title = "Context menu") { CustomTextMenuProvider { Column { SelectionContainer { Text("Hello, Compose!") } var text by remember { mutableStateOf("") } TextField(text, { text = it }) } } } @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 ) { // Reuses original TextContextMenu and adds a new item 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)}..."
Взаимодействие со Swing
Если вы встраиваете код Compose в существующее приложение Swing и хотите, чтобы контекстное меню соответствовало внешнему виду и поведению других частей приложения, можно использовать класс JPopupTextMenu. В этом классе LocalTextContextMenu использует Swing JPopupMenu для контекстных меню в компонентах Compose.
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, World!") } var text by remember { mutableStateOf("") } TextField(text, { text = it }) } } } val window = JFrame() window.contentPane.add(panel) window.size = Dimension(800, 600) window.isVisible = true window.title = "Swing interop" } @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) ) } // Adds items that can be defined via ContextMenuDataProvider in other parts of the application 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(8, 2) fillOval(0, 0, 16, 16) } } override fun getIconWidth() = 16 override fun getIconHeight() = 16 }