Skip to content

コンテキストメニュー

デスクトップ用 Compose Multiplatform は、テキストコンテキストメニューを標準でサポートしており、項目の追加、テーマの設定、テキストのカスタマイズによって、あらゆるコンテキストメニューを便利に調整できます。

カスタム領域のコンテキストメニュー

アプリケーションの任意の領域に対してコンテキストメニューを作成できます。ContextMenuArea を使用して、マウスの右クリックによってコンテキストメニューが表示されるコンテナを定義します。

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("ユーザー定義のアクション") {
                // カスタムアクション
            },
            ContextMenuItem("別のユーザー定義のアクション") {
                // 別のカスタムアクション
            }
        )
    }) {
        // コンテキストメニューが利用可能な青いボックス
        Box(modifier = Modifier.background(Color.Blue).height(100.dp).width(100.dp))
    }
}
Context menu: ContextMenuArea

テーマの設定

システム設定に合わせたレスポンシブな UI を作成し、アプリケーション間の切り替え時に激しいコントラストの変化を避けるために、コンテキストメニューの色をカスタマイズできます。デフォルトのライトテーマとダークテーマ向けに、LightDefaultContextMenuRepresentationDarkDefaultContextMenuRepresentation という 2 つの組み込み実装が用意されています。これらはコンテキストメニューの色に自動的には適用されないため、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(title = "Dark theme") {
    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 })
                }
            }
        }
    }
}
Context menu: Dark theme

メニュー項目のローカライズ

デフォルトでは、コンテキストメニューはシステム設定で優先されている言語で表示されます。

Context menu: Localization

特定の言語を使用したい場合は、アプリケーションを実行する前にデフォルト言語として明示的に指定してください。

Console
java.util.Locale.setDefault(java.util.Locale("en"))

テキストコンテキストメニュー

デフォルトのテキストコンテキストメニュー

デスクトップ用 Compose Multiplatform は、TextField および選択可能な Text 用の組み込みコンテキストメニューを提供します。

テキストフィールドのデフォルトのコンテキストメニューには、カーソルの位置や選択範囲に応じて、コピー、切り取り、貼り付け、すべて選択の各アクションが含まれます。この標準的なコンテキストメニューは、Material の TextFieldandroidx.compose.material.TextField または androidx.compose.material3.TextField)および Foundation の BasicTextFieldandroidx.compose.foundation.text.BasicTextField)でデフォルトで利用可能です。

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") }
    )
}
Default context menu for TextField

詳細は API リファレンス を参照してください。

単純なテキスト要素のデフォルトのコンテキストメニューには、コピーアクションのみが含まれます。Text コンポーネントでコンテキストメニューを有効にするには、テキストを SelectionContainer で囲んで選択可能にします。

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!")
    }
}
Default context menu for Text

カスタム項目の追加

TextField および Text コンポーネントにカスタムのコンテキストメニューアクションを追加するには、ContextMenuItem を介して新しい項目を指定し、ContextMenuDataProvider を介してそれらをコンテキストメニュー項目の階層に追加します。たとえば、以下のコードサンプルは、テキストフィールドと単純な選択可能テキスト要素のデフォルトコンテキストメニューに、2 つの新しいカスタムアクションを追加する方法を示しています。

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("ユーザー定義のアクション") {
                        // カスタムアクション
                    },
                    ContextMenuItem("別のユーザー定義のアクション") {
                        // 別のカスタムアクション
                    }
                )
            }
        ) {
            TextField(
                value = text.value,
                onValueChange = { text.value = it },
                label = { Text(text = "Input") }
            )

            Spacer(Modifier.height(16.dp))

            SelectionContainer {
                Text("Hello World!")
            }
        }
    }
}
Context menu with custom actions

デフォルトのテキストコンテキストメニューのオーバーライド

テキストフィールドおよび選択可能テキスト要素のデフォルトコンテキストメニューをオーバーライドするには、TextContextMenu インターフェースをオーバーライドします。以下のコードサンプルでは、元の TextContextMenu を再利用しつつ、リストの最後に 1 つの項目を追加しています。この新しい項目は、テキストの選択状態に応じて調整されます。

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.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
            ) {
                // 元の TextContextMenu を再利用し、新しい項目を追加する
                ContextMenuDataProvider({
                    val shortText = textManager.selectedText.crop()
                    if (shortText.isNotEmpty()) {
                        val encoded = URLEncoder.encode(shortText, Charset.defaultCharset())
                        listOf(ContextMenuItem("$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)}..."
Context menu: LocalTextContextMenu

Swing との相互運用性

Compose のコードを既存の Swing アプリケーションに埋め込んでおり、コンテキストメニューをアプリケーションの他の部分の外観や動作に合わせる必要がある場合は、JPopupTextMenu クラスを使用できます。このクラスでは、LocalTextContextMenu は Compose コンポーネント内のコンテキストメニューに Swing の JPopupMenu を使用します。

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, 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)
                    )
                }

                // アプリケーションの他の部分で 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(8, 2)
            fillOval(0, 0, 16, 16)
        }
    }

    override fun getIconWidth() = 16

    override fun getIconHeight() = 16
}
Context menu: Swing interoperability

次のステップ

他のデスクトップコンポーネントに関するチュートリアルをご覧ください。