1+ package com.example.compose.snippets.text
2+
3+ import android.os.Build
4+ import android.view.ActionMode
5+ import android.view.Menu
6+ import android.view.MenuItem
7+ import android.view.View
8+ import androidx.annotation.RequiresApi
9+ import androidx.compose.foundation.layout.padding
10+ import androidx.compose.foundation.text.selection.SelectionContainer
11+ import androidx.compose.material3.Text
12+ import androidx.compose.runtime.Composable
13+ import androidx.compose.runtime.CompositionLocalProvider
14+ import androidx.compose.ui.Modifier
15+ import androidx.compose.ui.geometry.Rect
16+ import androidx.compose.ui.platform.LocalTextToolbar
17+ import androidx.compose.ui.platform.LocalView
18+ import androidx.compose.ui.platform.TextToolbar
19+ import androidx.compose.ui.platform.TextToolbarStatus
20+ import androidx.compose.ui.tooling.preview.Preview
21+ import androidx.compose.ui.unit.dp
22+ import com.example.compose.snippets.R
23+ import com.example.compose.snippets.touchinput.userinteractions.MyAppTheme
24+
25+ @Preview
26+ @Composable
27+ private fun TextSelectionCustomActionPreview () {
28+ MyAppTheme {
29+ TextSelectionCustomAction ()
30+ }
31+ }
32+
33+ @Composable
34+ fun TextSelectionCustomAction (modifier : Modifier = Modifier ) {
35+ val textToolbar = CustomTextToolbar (
36+ view = LocalView .current,
37+ onCustomActionRequested = {
38+ // Handle the custom action
39+ }
40+ )
41+
42+ CompositionLocalProvider (LocalTextToolbar provides textToolbar) {
43+ SelectionContainer {
44+ Text (
45+ text = " This text is selectable" ,
46+ modifier = modifier.padding(16 .dp)
47+ )
48+ }
49+ }
50+ }
51+
52+ class CustomTextToolbar (
53+ private val view : View ,
54+ onCustomActionRequested : (() -> Unit )
55+ ) : TextToolbar {
56+ private var actionMode: ActionMode ? = null
57+ private val textActionModeCallback: TextActionModeCallback =
58+ TextActionModeCallback (
59+ onActionModeDestroy = { actionMode = null },
60+ onCustomActionRequested = onCustomActionRequested
61+ )
62+ override var status: TextToolbarStatus = TextToolbarStatus .Hidden
63+ private set
64+
65+ override fun showMenu (
66+ rect : Rect ,
67+ onCopyRequested : (() -> Unit )? ,
68+ onPasteRequested : (() -> Unit )? ,
69+ onCutRequested : (() -> Unit )? ,
70+ onSelectAllRequested : (() -> Unit )?
71+ ) {
72+ textActionModeCallback.rect = rect
73+ textActionModeCallback.onCopyRequested = onCopyRequested
74+ textActionModeCallback.onCutRequested = onCutRequested
75+ textActionModeCallback.onPasteRequested = onPasteRequested
76+ textActionModeCallback.onSelectAllRequested = onSelectAllRequested
77+ if (actionMode == null ) {
78+ status = TextToolbarStatus .Shown
79+ actionMode = if (Build .VERSION .SDK_INT >= 23 ) {
80+ view.startActionMode(
81+ FloatingTextActionModeCallback (
82+ textActionModeCallback
83+ ),
84+ ActionMode .TYPE_FLOATING
85+ )
86+ } else {
87+ view.startActionMode(
88+ PrimaryTextActionModeCallback (
89+ textActionModeCallback
90+ )
91+ )
92+ }
93+ } else {
94+ actionMode?.invalidate()
95+ }
96+ }
97+
98+ override fun hide () {
99+ status = TextToolbarStatus .Hidden
100+ actionMode?.finish()
101+ actionMode = null
102+ }
103+ }
104+
105+ internal class TextActionModeCallback (
106+ // The custom action callback
107+ val onCustomActionRequested : (() -> Unit ),
108+ // Existing parameters, copied from the default AndroidTextToolbar implementation
109+ val onActionModeDestroy : (() -> Unit )? = null ,
110+ var rect : Rect = Rect .Zero ,
111+ var onCopyRequested : (() -> Unit )? = null ,
112+ var onPasteRequested : (() -> Unit )? = null ,
113+ var onCutRequested : (() -> Unit )? = null ,
114+ var onSelectAllRequested : (() -> Unit )? = null ,
115+ ) {
116+ fun onCreateActionMode (mode : ActionMode ? , menu : Menu ? ): Boolean {
117+ requireNotNull(menu) { " onCreateActionMode requires a non-null menu" }
118+ requireNotNull(mode) { " onCreateActionMode requires a non-null mode" }
119+
120+ // Always add the custom menu item
121+ addMenuItem(menu, MenuItemOption .Custom )
122+
123+ // Default menu items if available
124+ onCopyRequested?.let {
125+ addMenuItem(menu, MenuItemOption .Copy )
126+ }
127+ onPasteRequested?.let {
128+ addMenuItem(menu, MenuItemOption .Paste )
129+ }
130+ onCutRequested?.let {
131+ addMenuItem(menu, MenuItemOption .Cut )
132+ }
133+ onSelectAllRequested?.let {
134+ addMenuItem(menu, MenuItemOption .SelectAll )
135+ }
136+ return true
137+ }
138+
139+ // this method is called to populate new menu items when the actionMode was invalidated
140+ fun onPrepareActionMode (mode : ActionMode ? , menu : Menu ? ): Boolean {
141+ if (mode == null || menu == null ) return false
142+ updateMenuItems(menu)
143+ // should return true so that new menu items are populated
144+ return true
145+ }
146+
147+ fun onActionItemClicked (mode : ActionMode ? , item : MenuItem ? ): Boolean {
148+ when (item!! .itemId) {
149+ // Call the custom action callback
150+ MenuItemOption .Custom .id -> onCustomActionRequested.invoke()
151+ // The default action callbacks
152+ MenuItemOption .Copy .id -> onCopyRequested?.invoke()
153+ MenuItemOption .Paste .id -> onPasteRequested?.invoke()
154+ MenuItemOption .Cut .id -> onCutRequested?.invoke()
155+ MenuItemOption .SelectAll .id -> onSelectAllRequested?.invoke()
156+ else -> return false
157+ }
158+ mode?.finish()
159+ return true
160+ }
161+
162+ fun onDestroyActionMode () {
163+ onActionModeDestroy?.invoke()
164+ }
165+
166+ internal fun updateMenuItems (menu : Menu ) {
167+ addOrRemoveMenuItem(menu, MenuItemOption .Custom , onCustomActionRequested)
168+ addOrRemoveMenuItem(menu, MenuItemOption .Copy , onCopyRequested)
169+ addOrRemoveMenuItem(menu, MenuItemOption .Paste , onPasteRequested)
170+ addOrRemoveMenuItem(menu, MenuItemOption .Cut , onCutRequested)
171+ addOrRemoveMenuItem(menu, MenuItemOption .SelectAll , onSelectAllRequested)
172+ }
173+
174+ internal fun addMenuItem (menu : Menu , item : MenuItemOption ) {
175+ menu.add(0 , item.id, item.order, item.titleResource)
176+ .setShowAsAction(MenuItem .SHOW_AS_ACTION_IF_ROOM )
177+ }
178+
179+ private fun addOrRemoveMenuItem (
180+ menu : Menu ,
181+ item : MenuItemOption ,
182+ callback : (() -> Unit )?
183+ ) {
184+ when {
185+ callback != null && menu.findItem(item.id) == null -> addMenuItem(menu, item)
186+ callback == null && menu.findItem(item.id) != null -> menu.removeItem(item.id)
187+ }
188+ }
189+ }
190+
191+ internal enum class MenuItemOption (val id : Int ) {
192+ // The added custom item
193+ Custom (0 ),
194+ // The default items, copied from the default AndroidTextToolbar implementation
195+ Copy (1 ),
196+ Paste (2 ),
197+ Cut (3 ),
198+ SelectAll (4 );
199+
200+ val titleResource: Int
201+ get() = when (this ) {
202+ Custom -> R .string.custom
203+ Copy -> android.R .string.copy
204+ Paste -> android.R .string.paste
205+ Cut -> android.R .string.cut
206+ SelectAll -> android.R .string.selectAll
207+ }
208+
209+ /* *
210+ * This item will be shown before all items that have order greater than this value.
211+ */
212+ val order = id
213+ }
214+
215+
216+ @RequiresApi(23 )
217+ internal class FloatingTextActionModeCallback (
218+ private val callback : TextActionModeCallback
219+ ) : ActionMode.Callback2() {
220+ override fun onActionItemClicked (mode : ActionMode ? , item : MenuItem ? ): Boolean {
221+ return callback.onActionItemClicked(mode, item)
222+ }
223+
224+ override fun onCreateActionMode (mode : ActionMode ? , menu : Menu ? ): Boolean {
225+ return callback.onCreateActionMode(mode, menu)
226+ }
227+
228+ override fun onPrepareActionMode (mode : ActionMode ? , menu : Menu ? ): Boolean {
229+ return callback.onPrepareActionMode(mode, menu)
230+ }
231+
232+ override fun onDestroyActionMode (mode : ActionMode ? ) {
233+ callback.onDestroyActionMode()
234+ }
235+
236+ override fun onGetContentRect (
237+ mode : ActionMode ? ,
238+ view : View ? ,
239+ outRect : android.graphics.Rect ?
240+ ) {
241+ val rect = callback.rect
242+ outRect?.set(
243+ rect.left.toInt(),
244+ rect.top.toInt(),
245+ rect.right.toInt(),
246+ rect.bottom.toInt()
247+ )
248+ }
249+ }
250+
251+ internal class PrimaryTextActionModeCallback (
252+ private val callback : TextActionModeCallback
253+ ) : ActionMode.Callback {
254+ override fun onActionItemClicked (mode : ActionMode ? , item : MenuItem ? ): Boolean {
255+ return callback.onActionItemClicked(mode, item)
256+ }
257+
258+ override fun onCreateActionMode (mode : ActionMode ? , menu : Menu ? ): Boolean {
259+ return callback.onCreateActionMode(mode, menu)
260+ }
261+
262+ override fun onPrepareActionMode (mode : ActionMode ? , menu : Menu ? ): Boolean {
263+ return callback.onPrepareActionMode(mode, menu)
264+ }
265+
266+ override fun onDestroyActionMode (mode : ActionMode ? ) {
267+ callback.onDestroyActionMode()
268+ }
269+ }
0 commit comments