Skip to content

Commit 2f629de

Browse files
committed
Improve context menu
One unified context menu, simpler way to provide new items
1 parent 6785973 commit 2f629de

15 files changed

Lines changed: 173 additions & 338 deletions

File tree

ComposeTextEditor/src/androidMain/kotlin/com/darkrockstudios/texteditor/contextmenu/TextEditorContextMenuProvider.android.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ actual fun TextEditorContextMenuProvider(
2222
actions = actions,
2323
strings = strings,
2424
enabled = enabled,
25+
extraItems = menuState.extraItems.value,
2526
onDismiss = menuState::dismissMenu
2627
)
2728
}

ComposeTextEditor/src/commonMain/kotlin/com/darkrockstudios/texteditor/BasicTextEditor.kt

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ fun BasicTextEditor(
4444
autoFocus: Boolean = false,
4545
style: TextEditorStyle = rememberTextEditorStyle(),
4646
contextMenuStrings: ContextMenuStrings = ContextMenuStrings.Default,
47+
contextMenuState: TextEditorContextMenuState? = null,
4748
onRichSpanClick: RichSpanClickListener? = null,
4849
decorateLine: LineDecorator? = null,
4950
) {
@@ -58,7 +59,10 @@ fun BasicTextEditor(
5859
TextEditorInputModifierElement(state, clipboard, enabled)
5960
}
6061

61-
val contextMenuState = remember { TextEditorContextMenuState() }
62+
// Use provided context menu state or create internal one
63+
val internalContextMenuState = remember { TextEditorContextMenuState() }
64+
val effectiveContextMenuState = contextMenuState ?: internalContextMenuState
65+
6266
val contextMenuActions = remember(state, clipboard) {
6367
ContextMenuActions(state, clipboard, state.scope)
6468
}
@@ -84,7 +88,7 @@ fun BasicTextEditor(
8488
}
8589

8690
TextEditorContextMenuProvider(
87-
menuState = contextMenuState,
91+
menuState = effectiveContextMenuState,
8892
actions = contextMenuActions,
8993
strings = contextMenuStrings,
9094
enabled = enabled,
@@ -118,9 +122,7 @@ fun BasicTextEditor(
118122
.textEditorPointerInputHandling(
119123
state = state,
120124
onSpanClick = onRichSpanClick,
121-
onContextMenuRequest = { offset ->
122-
contextMenuState.showMenu(offset)
123-
}
125+
onContextMenuRequest = { offset -> effectiveContextMenuState.showMenu(offset) }
124126
)
125127
.size(
126128
width = state.viewportSize.width.dp,

ComposeTextEditor/src/commonMain/kotlin/com/darkrockstudios/texteditor/contextmenu/TextEditorContextMenu.kt

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import androidx.compose.foundation.layout.Box
44
import androidx.compose.foundation.layout.offset
55
import androidx.compose.material3.DropdownMenu
66
import androidx.compose.material3.DropdownMenuItem
7+
import androidx.compose.material3.HorizontalDivider
78
import androidx.compose.material3.Text
89
import androidx.compose.runtime.Composable
910
import androidx.compose.ui.Modifier
@@ -13,11 +14,13 @@ import kotlin.math.roundToInt
1314

1415
/**
1516
* Context menu dropdown for Cut, Copy, Paste, and Select All operations.
17+
* Supports extra items that appear before the standard items.
1618
*
1719
* @param position The position where the menu should appear
1820
* @param actions The context menu actions handler
1921
* @param strings Localizable strings for menu items
2022
* @param enabled Whether editing operations (cut, paste) are enabled
23+
* @param extraItems Extra menu items to display before standard items
2124
* @param onDismiss Callback when the menu should be dismissed
2225
*/
2326
@Composable
@@ -26,6 +29,7 @@ internal fun TextEditorContextMenu(
2629
actions: ContextMenuActions,
2730
strings: ContextMenuStrings,
2831
enabled: Boolean,
32+
extraItems: List<ContextMenuItem> = emptyList(),
2933
onDismiss: () -> Unit,
3034
) {
3135
Box(modifier = Modifier.offset {
@@ -38,6 +42,23 @@ internal fun TextEditorContextMenu(
3842
expanded = true,
3943
onDismissRequest = onDismiss,
4044
) {
45+
// Extra items first (e.g., spell check suggestions)
46+
extraItems.forEach { item ->
47+
DropdownMenuItem(
48+
text = { Text(item.label) },
49+
enabled = item.enabled,
50+
onClick = {
51+
item.onClick()
52+
onDismiss()
53+
},
54+
)
55+
}
56+
57+
// Divider between extra items and standard items
58+
if (extraItems.isNotEmpty()) {
59+
HorizontalDivider()
60+
}
61+
4162
// Cut - requires selection and enabled
4263
if (actions.canCut() && enabled) {
4364
DropdownMenuItem(

ComposeTextEditor/src/commonMain/kotlin/com/darkrockstudios/texteditor/contextmenu/TextEditorContextMenuState.kt

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,31 @@ import androidx.compose.runtime.MutableState
44
import androidx.compose.runtime.mutableStateOf
55
import androidx.compose.ui.geometry.Offset
66

7+
/**
8+
* Represents a custom menu item that can be added to the context menu.
9+
*/
10+
data class ContextMenuItem(
11+
val label: String,
12+
val enabled: Boolean = true,
13+
val onClick: () -> Unit
14+
)
15+
716
/**
817
* State holder for the text editor context menu.
9-
* Tracks whether the menu is visible and its position.
18+
* Tracks whether the menu is visible, its position, and any extra menu items.
1019
*/
1120
class TextEditorContextMenuState {
1221
/**
1322
* The position where the menu should be displayed, or null if hidden.
1423
*/
1524
val menuPosition: MutableState<Offset?> = mutableStateOf(null)
1625

26+
/**
27+
* Extra menu items to display before the standard items (Cut, Copy, Paste, Select All).
28+
* These are rendered first, followed by a divider if non-empty.
29+
*/
30+
val extraItems: MutableState<List<ContextMenuItem>> = mutableStateOf(emptyList())
31+
1732
/**
1833
* Whether the menu is currently visible.
1934
*/
@@ -28,9 +43,18 @@ class TextEditorContextMenuState {
2843
}
2944

3045
/**
31-
* Dismiss the context menu.
46+
* Show the context menu at the specified position with extra items.
47+
*/
48+
fun showMenu(position: Offset, items: List<ContextMenuItem>) {
49+
extraItems.value = items
50+
menuPosition.value = position
51+
}
52+
53+
/**
54+
* Dismiss the context menu and clear extra items.
3255
*/
3356
fun dismissMenu() {
3457
menuPosition.value = null
58+
extraItems.value = emptyList()
3559
}
3660
}

ComposeTextEditor/src/desktopMain/kotlin/com/darkrockstudios/texteditor/contextmenu/TextEditorContextMenuProvider.desktop.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ actual fun TextEditorContextMenuProvider(
2222
actions = actions,
2323
strings = strings,
2424
enabled = enabled,
25+
extraItems = menuState.extraItems.value,
2526
onDismiss = menuState::dismissMenu
2627
)
2728
}

ComposeTextEditor/src/iosMain/kotlin/com/darkrockstudios/texteditor/contextmenu/TextEditorContextMenuProvider.ios.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ actual fun TextEditorContextMenuProvider(
2222
actions = actions,
2323
strings = strings,
2424
enabled = enabled,
25+
extraItems = menuState.extraItems.value,
2526
onDismiss = menuState::dismissMenu
2627
)
2728
}

ComposeTextEditor/src/wasmJsMain/kotlin/com/darkrockstudios/texteditor/contextmenu/TextEditorContextMenuProvider.wasmJs.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ actual fun TextEditorContextMenuProvider(
2222
actions = actions,
2323
strings = strings,
2424
enabled = enabled,
25+
extraItems = menuState.extraItems.value,
2526
onDismiss = menuState::dismissMenu
2627
)
2728
}

ComposeTextEditorSpellCheck/src/androidMain/kotlin/com/darkrockstudios/texteditor/spellcheck/SpellCheckTextContextMenuProvider.android.kt

Lines changed: 0 additions & 27 deletions
This file was deleted.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package com.darkrockstudios.texteditor.spellcheck
2+
3+
import com.darkrockstudios.texteditor.spellcheck.api.Correction
4+
import com.darkrockstudios.texteditor.state.WordSegment
5+
6+
/**
7+
* Represents a spell check item that can be either a word-level misspelling or a sentence-level correction.
8+
*/
9+
sealed class SpellCheckItem {
10+
data class MisspelledWord(val segment: WordSegment) : SpellCheckItem()
11+
data class SentenceIssue(val correction: Correction) : SpellCheckItem()
12+
}

ComposeTextEditorSpellCheck/src/commonMain/kotlin/com/darkrockstudios/texteditor/spellcheck/SpellCheckTextContextMenuProvider.kt

Lines changed: 0 additions & 57 deletions
This file was deleted.

0 commit comments

Comments
 (0)