Skip to content

Commit

Permalink
Fixed appearing of text editing menu in Selection Container in iOS (#…
Browse files Browse the repository at this point in the history
…1269)

## Proposed Changes

- Added creating a text interaction view (which is required to show the
edit menu by long press / double tap on the text) in case when there is
no one (i.e. only selection container presents of the screen without any
other text editing views)
- Fixed its position on the screen (changes from androidx:
#1270)

## Testing

Test: Open test app, go Components -> Selection, try to select text in
selection container

## Issues Fixed

Fixes: 
-
https://youtrack.jetbrains.com/issue/COMPOSE-1190/iOS-Selection-Container-cant-show-options-menu
- JetBrains/compose-multiplatform#4322

## Google CLA
You need to sign the Google Contributor’s License Agreement at
https://cla.developers.google.com/.
This is needed since we synchronise most of the code with Google’s AOSP
repository. Signing this agreement allows us to synchronise code from
your Pull Requests as well.
  • Loading branch information
mazunin-v-jb committed Apr 19, 2024
1 parent a5c2773 commit 73ed2f5
Show file tree
Hide file tree
Showing 3 changed files with 117 additions and 85 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,10 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.selection.DisableSelection
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.TextField
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.mpp.demo.textfield.ClearFocusBox
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
Expand All @@ -40,46 +42,60 @@ import androidx.compose.ui.unit.dp
@Composable
fun SelectionExample() {
var count by remember { mutableStateOf(0) }
Column {
Button(onClick = { count++ }) {
Text("Outside Count: $count")
}
SelectionContainer(
Modifier.padding(24.dp).fillMaxWidth()
) {
Column {
Text(
"I'm a selection container. Double tap on word to select a word." +
" Triple tap on content to select whole paragraph.\nAnother paragraph for testing.\n" +
"And another one."
)
Row {
DisableSelection {
val textState = remember {
mutableStateOf(
buildString {
repeat(3) {
appendLine("Text line $it")
}
}
)
}
ClearFocusBox {
Column {
Button(onClick = { count++ }) {
Text("Outside Count: $count")
}
SelectionContainer(
Modifier.padding(24.dp).fillMaxWidth()
) {
Column {
TextField(
textState.value, { textState.value = it },
)
Text(
"I'm a selection container. Double tap on word to select a word." +
" Triple tap on content to select whole paragraph.\nAnother paragraph for testing.\n" +
"And another one."
)
Row {
DisableSelection {
Button(onClick = { count++ }) {
Text("DisableSelection Count: $count")
}
}
Button(onClick = { count++ }) {
Text("DisableSelection Count: $count")
Text("SelectionContainer Count: $count")
}
}
Button(onClick = { count++ }) {
Text("SelectionContainer Count: $count")
}
Text("I'm another Text() block. Let's try to select me!")
Text("I'm yet another Text() with multiparagraph structure block.\nLet's try to select me!")
}
Text("I'm another Text() block. Let's try to select me!")
Text("I'm yet another Text() with multiparagraph structure block.\nLet's try to select me!")
}
}
Column(
Modifier
.height(100.dp)
.padding(2.dp)
.border(1.dp, Color.Blue)
.fillMaxWidth()
.verticalScroll(rememberScrollState())
) {
SelectionContainer {
Text(
text = "Select text and scroll\n".repeat(100),
modifier = Modifier.fillMaxWidth(),
)
Column(
Modifier
.height(100.dp)
.padding(2.dp)
.border(1.dp, Color.Blue)
.fillMaxWidth()
.verticalScroll(rememberScrollState())
) {
SelectionContainer {
Text(
text = "Select text and scroll\n".repeat(100),
modifier = Modifier.fillMaxWidth(),
)
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,32 @@ import androidx.compose.ui.input.key.KeyEvent
import androidx.compose.ui.input.key.KeyEventType
import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.type
import androidx.compose.ui.text.input.*
import androidx.compose.ui.scene.getConstraintsToFillParent
import androidx.compose.ui.text.input.CommitTextCommand
import androidx.compose.ui.text.input.EditCommand
import androidx.compose.ui.text.input.EditProcessor
import androidx.compose.ui.text.input.FinishComposingTextCommand
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.ImeOptions
import androidx.compose.ui.text.input.PlatformTextInputService
import androidx.compose.ui.text.input.SetComposingRegionCommand
import androidx.compose.ui.text.input.SetComposingTextCommand
import androidx.compose.ui.text.input.SetSelectionCommand
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.asCGRect
import androidx.compose.ui.unit.toDpRect
import androidx.compose.ui.window.FocusStack
import androidx.compose.ui.window.IntermediateTextInputUIView
import androidx.compose.ui.window.KeyboardEventHandler
import androidx.compose.ui.scene.getConstraintsToFillParent
import androidx.compose.ui.unit.Density
import kotlin.math.absoluteValue
import kotlin.math.min
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
import org.jetbrains.skia.BreakIterator
import platform.UIKit.*
import platform.UIKit.NSLayoutConstraint
import platform.UIKit.UIView
import platform.UIKit.reloadInputViews

internal class UIKitTextInputService(
private val updateView: () -> Unit,
Expand Down Expand Up @@ -111,17 +125,7 @@ internal class UIKitTextInputService(
currentImeOptions = imeOptions
currentImeActionHandler = onImeActionPerformed

textUIView?.removeFromSuperview()
textUIView = IntermediateTextInputUIView(
viewConfiguration = viewConfiguration
).also {
it.keyboardEventHandler = keyboardEventHandler
rootView.addSubview(it)
it.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activateConstraints(
getConstraintsToFillParent(it, rootView)
)
}
attachIntermediateTextInputView()
textUIView?.input = createSkikoInput(value)
textUIView?.inputTraits = getUITextInputTraits(imeOptions)

Expand All @@ -137,14 +141,7 @@ internal class UIKitTextInputService(

textUIView?.inputTraits = EmptyInputTraits
textUIView?.input = null
textUIView?.keyboardEventHandler = null
textUIView?.let { view ->
mainScope.launch {
view.resignFirstResponder()
view.removeFromSuperview()
}
}
textUIView = null
detachIntermediateTextInputView()
}

override fun showSoftwareKeyboard() {
Expand Down Expand Up @@ -274,32 +271,34 @@ internal class UIKitTextInputService(
onCutRequested: (() -> Unit)?,
onSelectAllRequested: (() -> Unit)?
) {
textUIView?.let {
val skiaRect = with(densityProvider()) {
org.jetbrains.skia.Rect.makeLTRB(
l = rect.left / density,
t = rect.top / density,
r = rect.right / density,
b = rect.bottom / density,
)
}
it.showTextMenu(
targetRect = skiaRect,
textActions = object : TextActions {
override val copy: (() -> Unit)? = onCopyRequested
override val cut: (() -> Unit)? = onCutRequested
override val paste: (() -> Unit)? = onPasteRequested
override val selectAll: (() -> Unit)? = onSelectAllRequested
}
)
if (textUIView == null) {
// If showMenu() is called and textUIView is not created,
// then it means that showMenu() called in SelectionContainer without any textfields,
// and IntermediateTextInputView must be created to show an editing menu
attachIntermediateTextInputView()
textUIView?.becomeFirstResponder()
updateView()
}
textUIView?.showTextMenu(
targetRect = rect.toDpRect(densityProvider()).asCGRect(),
textActions = object : TextActions {
override val copy: (() -> Unit)? = onCopyRequested
override val cut: (() -> Unit)? = onCutRequested
override val paste: (() -> Unit)? = onPasteRequested
override val selectAll: (() -> Unit)? = onSelectAllRequested
}
)
}

/**
* TODO on UIKit native behaviour is hide text menu, when touch outside
*/
override fun hide() {
textUIView?.hideTextMenu()
if ((textUIView != null) && (currentInput == null)) { // means that editing context menu shown in selection container
textUIView?.resignFirstResponder()
detachIntermediateTextInputView()
}
}

override val status: TextToolbarStatus
Expand All @@ -308,6 +307,29 @@ internal class UIKitTextInputService(
else
TextToolbarStatus.Hidden

private fun attachIntermediateTextInputView() {
textUIView?.removeFromSuperview()
textUIView = IntermediateTextInputUIView(
viewConfiguration = viewConfiguration
).also {
it.keyboardEventHandler = keyboardEventHandler
rootView.addSubview(it)
it.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activateConstraints(
getConstraintsToFillParent(it, rootView)
)
}
}

private fun detachIntermediateTextInputView() {
textUIView?.let { view ->
view.keyboardEventHandler = null
mainScope.launch {
view.removeFromSuperview()
}
}
textUIView = null
}

private fun createSkikoInput(value: TextFieldValue) = object : IOSSkikoInput {
/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -501,14 +501,8 @@ internal class IntermediateTextInputUIView(
* @param targetRect - rectangle of selected text area
* @param textActions - available (not null) actions in text menu
*/
fun showTextMenu(targetRect: org.jetbrains.skia.Rect, textActions: TextActions) {
val cgRect = CGRectMake(
x = targetRect.left.toDouble(),
y = targetRect.top.toDouble(),
width = targetRect.width.toDouble(),
height = targetRect.height.toDouble()
)
val isTargetVisible = CGRectIntersectsRect(bounds, cgRect)
fun showTextMenu(targetRect: CValue<CGRect>, textActions: TextActions) {
val isTargetVisible = CGRectIntersectsRect(bounds, targetRect)

if (isTargetVisible) {
// TODO: UIMenuController is deprecated since iOS 17 and not available on iOS 12
Expand All @@ -519,7 +513,7 @@ internal class IntermediateTextInputUIView(
cancelContextMenuUpdate()
CoroutineScope(Dispatchers.Main + menuMonitoringJob).launch {
delay(viewConfiguration.doubleTapTimeoutMillis)
menu.showMenuFromView(targetView = this@IntermediateTextInputUIView, cgRect)
menu.showMenuFromView(targetView = this@IntermediateTextInputUIView, targetRect)
}
_currentTextMenuActions = textActions
} else {
Expand Down

0 comments on commit 73ed2f5

Please sign in to comment.