Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -265,15 +265,17 @@ fun CustomTextMenuProvider(content: @Composable () -> Unit) {

private fun AnnotatedString.crop() = if (length <= 5) toString() else "${take(5)}..."

@OptIn(ExperimentalFoundationApi::class)
private fun swingItem(
label: String,
color: java.awt.Color,
key: Int,
onClick: () -> Unit
menuItemAction: TextContextMenu.Action
) = JMenuItem(label).apply {
icon = circleIcon(color)
accelerator = KeyStroke.getKeyStroke(key, if (hostOs.isMacOS) META_DOWN_MASK else CTRL_DOWN_MASK)
addActionListener { onClick() }
isEnabled = menuItemAction.enabled
addActionListener { menuItemAction.execute() }
}

private fun circleIcon(color: java.awt.Color) = object : Icon {
Expand Down
5 changes: 4 additions & 1 deletion compose/foundation/foundation/api/desktop/foundation.api
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ public final class androidx/compose/foundation/ContextMenuData {
public class androidx/compose/foundation/ContextMenuItem {
public static final field $stable I
public fun <init> (Ljava/lang/String;Lkotlin/jvm/functions/Function0;)V
public fun <init> (Ljava/lang/String;ZLkotlin/jvm/functions/Function0;)V
public final fun contextMenuItemBaseClassEnabled ()Z
public fun equals (Ljava/lang/Object;)Z
public final fun getLabel ()Ljava/lang/String;
public final fun getOnClick ()Lkotlin/jvm/functions/Function0;
Expand Down Expand Up @@ -137,7 +139,8 @@ public final class androidx/compose/foundation/DarkThemeKt {

public final class androidx/compose/foundation/DefaultContextMenuRepresentation : androidx/compose/foundation/ContextMenuRepresentation {
public static final field $stable I
public synthetic fun <init> (JJJLkotlin/jvm/internal/DefaultConstructorMarker;)V
public synthetic fun <init> (JJJJILkotlin/jvm/internal/DefaultConstructorMarker;)V
public synthetic fun <init> (JJJJLkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun Representation (Landroidx/compose/foundation/ContextMenuState;Lkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;I)V
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import androidx.compose.foundation.text.contextmenu.data.TextContextMenuItemWith
import androidx.compose.foundation.text.contextmenu.data.TextContextMenuSession
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
Expand Down Expand Up @@ -65,42 +66,48 @@ val DarkDefaultContextMenuRepresentation = DefaultContextMenuRepresentation(
class DefaultContextMenuRepresentation(
private val backgroundColor: Color,
private val textColor: Color,
private val itemHoverColor: Color
private val itemHoverColor: Color,
private val disabledTextColor: Color = textColor.copy(alpha = 0.38f),
) : ContextMenuRepresentation {
@OptIn(ExperimentalComposeUiApi::class)
@Composable
override fun Representation(state: ContextMenuState, items: () -> List<ContextMenuItem>) {
val status = state.status
if (status is ContextMenuState.Status.Open) {
val session = remember(state) {
object : TextContextMenuSession {
override fun close() {
state.status = ContextMenuState.Status.Closed
}
if (status !is ContextMenuState.Status.Open) return

val session = remember(state) {
object : TextContextMenuSession {
override fun close() {
state.status = ContextMenuState.Status.Closed
}
}
val components by remember {
derivedStateOf {
items().map {
TextContextMenuItemWithComposableLeadingIcon(
key = it,
label = it.label,
enabled = true,
onClick = {
session.close()
it.onClick()
}
)
}
}
val components by remember {
derivedStateOf {
items().map {
TextContextMenuItemWithComposableLeadingIcon(
key = it,
label = it.label,
enabled = it.enabled,
onClick = {
session.close()
it.onClick()
}
)
}
}
val colors = remember(backgroundColor, textColor, itemHoverColor) {
}

if (components.isEmpty()) {
SideEffect { session.close() }
} else {
val colors = remember(backgroundColor, textColor, itemHoverColor, disabledTextColor) {
ContextMenuColors(
backgroundColor = backgroundColor,
textColor = textColor,
iconColor = Color.Unspecified,
disabledTextColor = Color.Unspecified,
disabledIconColor = Color.Unspecified,
iconColor = textColor,
disabledTextColor = disabledTextColor,
disabledIconColor = disabledTextColor,
hoverColor = itemHoverColor,
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -186,32 +186,41 @@ private suspend fun AwaitPointerEventScope.awaitEventFirstDown(): PointerEvent {
* Individual element of context menu.
*
* @param label The text to be displayed as a context menu item.
* @param enabled Whether the item is enabled.
* @param onClick The action to be executed after click on the item.
*/
open class ContextMenuItem(
val label: String,
enabled: Boolean,
val onClick: () -> Unit
) {
constructor(label: String, onClick: () -> Unit) : this(label, true, onClick)

// Avoid breaking backwards compatibility with subclasses that already have an `enabled` field
private val _enabled = enabled
val enabled: Boolean
@JvmName("contextMenuItemBaseClassEnabled") get() = _enabled

override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class != other::class) return false

other as ContextMenuItem
if (other !is ContextMenuItem) return false

if (label != other.label) return false
if (enabled != other.enabled) return false
if (onClick != other.onClick) return false

return true
}

override fun hashCode(): Int {
var result = label.hashCode()
result = 31 * result + enabled.hashCode()
result = 31 * result + onClick.hashCode()
return result
}

override fun toString(): String {
return "ContextMenuItem(label='$label')"
return "ContextMenuItem(label='$label', enabled=$enabled)"
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,45 +137,41 @@ private val TextFieldSelectionManager.textManager: TextManager get() = object :

val isPassword get() = visualTransformation is PasswordVisualTransformation

override val cut: (() -> Unit)? get() =
if (!value.selection.collapsed && editable && !isPassword) {
{
override val cut: TextContextMenu.Action get() =
TextContextMenu.Action(
enabled = !value.selection.collapsed && editable && !isPassword,
execute = {
cut()
focusRequester?.requestFocus()
}
} else {
null
}
)

override val copy: (() -> Unit)? get() =
if (!value.selection.collapsed && !isPassword) {
{
override val copy: TextContextMenu.Action get() =
TextContextMenu.Action(
enabled = !value.selection.collapsed && !isPassword,
execute = {
copy(false)
focusRequester?.requestFocus()
}
} else {
null
}
)

override val paste: (() -> Unit)? get() =
if (editable && clipboard?.nativeClipboardHasText() == true) {
{
override val paste: TextContextMenu.Action get() =
TextContextMenu.Action(
enabled = editable && clipboard?.nativeClipboardHasText() == true,
execute = {
paste()
focusRequester?.requestFocus()
}
} else {
null
}
)

override val selectAll: (() -> Unit)? get() =
if (value.selection.length != value.text.length) {
{
override val selectAll: TextContextMenu.Action get() =
TextContextMenu.Action(
enabled = value.selection.length != value.text.length,
execute = {
selectAll()
focusRequester?.requestFocus()
}
} else {
null
}
)

override fun selectWordAtPositionIfNotAlreadySelected(offset: Offset) {
[email protected](offset)
Expand All @@ -198,20 +194,32 @@ private fun TextFieldSelectionState.textManager(coroutineScope: CoroutineScope):

private fun pasteImpl() = launchUndispatched { paste() }

override val cut: (() -> Unit)?
get() = if (canShowCutMenuItem()) ::cutImpl else null
override val cut: TextContextMenu.Action get() =
TextContextMenu.Action(
enabled = canShowCutMenuItem(),
execute = ::cutImpl
)

override val copy: (() -> Unit)?
get() = if (canShowCopyMenuItem()) ::copyImpl else null
override val copy: TextContextMenu.Action get() =
TextContextMenu.Action(
enabled = canShowCopyMenuItem(),
execute = ::copyImpl
)

override val paste: (() -> Unit)?
get() {
launchUndispatched { updateClipboardEntry() }
return if (canShowPasteMenuItem()) ::pasteImpl else null
}
override val paste: TextContextMenu.Action get() =
TextContextMenu.Action(
enabled = run {
launchUndispatched { updateClipboardEntry() }
canShowPasteMenuItem()
},
execute = ::pasteImpl
)

override val selectAll: (() -> Unit)?
get() = if (canShowSelectAllMenuItem()) ::selectAll else null
override val selectAll: TextContextMenu.Action get() =
TextContextMenu.Action(
enabled = canShowSelectAllMenuItem(),
execute = ::selectAll
)

override fun selectWordAtPositionIfNotAlreadySelected(offset: Offset) {
if (!textLayoutState.isPositionOnText(offset)) return
Expand All @@ -236,7 +244,11 @@ private fun TextFieldSelectionState.textManager(coroutineScope: CoroutineScope):
private val SelectionManager.textManager: TextManager get() = object : TextManager {
override val selectedText get() = getSelectedText() ?: AnnotatedString("")
override val cut = null
override val copy = { copy() }
override val copy get() =
TextContextMenu.Action(
enabled = isNonEmptySelection(),
execute = { copy()}
)
override val paste = null
override val selectAll = null
override fun selectWordAtPositionIfNotAlreadySelected(offset: Offset) {
Expand Down Expand Up @@ -283,22 +295,22 @@ interface TextContextMenu {
/**
* Action for cutting the selected text to the clipboard. Null if there is no text to cut.
*/
val cut: (() -> Unit)?
val cut: Action?

/**
* Action for copy the selected text to the clipboard. Null if there is no text to copy.
*/
val copy: (() -> Unit)?
val copy: Action?

/**
* Action for pasting text from the clipboard. Null if there is no text in the clipboard.
*/
val paste: (() -> Unit)?
val paste: Action?

/**
* Action for selecting the whole text. Null if the text is already selected.
*/
val selectAll: (() -> Unit)?
val selectAll: Action?

/**
* Selects the word at the given [offset], unless the current selection already encompasses
Expand All @@ -307,40 +319,56 @@ interface TextContextMenu {
fun selectWordAtPositionIfNotAlreadySelected(offset: Offset)
}

@ExperimentalFoundationApi
class Action(val enabled: Boolean, val execute: () -> Unit)

companion object {
/**
* [TextContextMenu] that is used by default in Compose.
*/
@ExperimentalFoundationApi
val Default = object : TextContextMenu {
@Composable
override fun Area(textManager: TextManager, state: ContextMenuState, content: @Composable () -> Unit) {
val localization = LocalLocalization.current
val items = {
listOfNotNull(
textManager.cut?.let {
ContextMenuItem(localization.cut, it)
},
textManager.copy?.let {
ContextMenuItem(localization.copy, it)
},
textManager.paste?.let {
ContextMenuItem(localization.paste, it)
},
textManager.selectAll?.let {
ContextMenuItem(localization.selectAll, it)
},
)
}

TextContextMenuArea(
textManager = textManager,
items = items,
state = state,
content = content
)
val Default: TextContextMenu = BasicTextContextMenu(showDisabledItems = true)

/**
* [TextContextMenu] that doesn't show any disabled items.
*/
@ExperimentalFoundationApi
val HideDisabledMenuItems: TextContextMenu = BasicTextContextMenu(showDisabledItems = false)
}
}

/**
* Basic implementation of [TextContextMenu].
*/
private class BasicTextContextMenu(
val showDisabledItems: Boolean
) : TextContextMenu {
@Composable
override fun Area(
textManager: TextManager,
state: ContextMenuState,
content: @Composable () -> Unit
) {
val localization = LocalLocalization.current
val items = {
listOf(
localization.cut to textManager.cut,
localization.copy to textManager.copy,
localization.paste to textManager.paste,
localization.selectAll to textManager.selectAll,
).mapNotNull { (localization, action) ->
if (action == null) return@mapNotNull null
if (!showDisabledItems && !action.enabled) return@mapNotNull null
ContextMenuItem(localization, action.enabled, action.execute)
}
}

TextContextMenuArea(
textManager = textManager,
items = items,
state = state,
content = content
)
}
}

Expand Down
Loading