1 /* 2 * Copyright 2019 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 @file:Suppress("DEPRECATION") 18 19 package androidx.compose.ui.text.input 20 21 import androidx.compose.ui.text.TextRange 22 import androidx.compose.ui.text.emptyAnnotatedString 23 import androidx.compose.ui.util.fastForEach 24 25 /** 26 * Helper class to apply [EditCommand]s on an internal buffer. Used by TextField Composable to 27 * combine TextFieldValue lifecycle with the editing operations. 28 * * When a [TextFieldValue] is suggested by the developer, [reset] should be called. 29 * * When [TextInputService] provides [EditCommand]s, they should be applied to the internal buffer 30 * using [apply]. 31 */ 32 class EditProcessor { 33 34 /** The current state of the internal editing buffer as a [TextFieldValue]. */ 35 /*@VisibleForTesting*/ 36 internal var mBufferState: TextFieldValue = 37 TextFieldValue(emptyAnnotatedString(), TextRange.Zero, null) 38 private set 39 40 // The editing buffer used for applying editor commands from IME. 41 /*@VisibleForTesting*/ 42 internal var mBuffer: EditingBuffer = 43 EditingBuffer(text = mBufferState.annotatedString, selection = mBufferState.selection) 44 private set 45 46 /** 47 * Must be called whenever new editor model arrives. 48 * 49 * This method updates the internal editing buffer with the given editor model. This method may 50 * tell the IME about the selection offset changes or extracted text changes. 51 */ 52 @Suppress("ReferencesDeprecated") resetnull53 fun reset( 54 value: TextFieldValue, 55 textInputSession: TextInputSession?, 56 ) { 57 var textChanged = false 58 var selectionChanged = false 59 val compositionChanged = value.composition != mBuffer.composition 60 61 if (mBufferState.annotatedString.text != value.annotatedString.text) { 62 mBuffer = EditingBuffer(text = value.annotatedString, selection = value.selection) 63 textChanged = true 64 } else if (mBufferState.selection != value.selection) { 65 mBuffer.setSelection(value.selection.min, value.selection.max) 66 selectionChanged = true 67 } 68 69 if (value.composition == null) { 70 mBuffer.commitComposition() 71 } else if (!value.composition.collapsed) { 72 mBuffer.setComposition(value.composition.min, value.composition.max) 73 } 74 75 // this is the same code as in TextInputServiceAndroid class where restartInput is decided 76 // if restartInput is going to be called the composition has to be cleared otherwise it 77 // results in keyboards behaving strangely. 78 val newValue = 79 if (textChanged || (!selectionChanged && compositionChanged)) { 80 mBuffer.commitComposition() 81 value.copy(composition = null) 82 } else { 83 value 84 } 85 86 val oldValue = mBufferState 87 mBufferState = newValue 88 89 textInputSession?.updateState(oldValue, newValue) 90 } 91 92 /** 93 * Applies a set of [editCommands] to the internal text editing buffer. 94 * 95 * After applying the changes, returns the final state of the editing buffer as a 96 * [TextFieldValue] 97 * 98 * @param editCommands [EditCommand]s to be applied to the editing buffer. 99 * @return the [TextFieldValue] representation of the final buffer state. 100 */ applynull101 fun apply(editCommands: List<EditCommand>): TextFieldValue { 102 var lastCommand: EditCommand? = null 103 try { 104 editCommands.fastForEach { 105 lastCommand = it 106 it.applyTo(mBuffer) 107 } 108 } catch (e: Exception) { 109 throw RuntimeException(generateBatchErrorMessage(editCommands, lastCommand), e) 110 } 111 112 val newState = 113 TextFieldValue( 114 annotatedString = mBuffer.toAnnotatedString(), 115 // preserve original reversed selection when creating new state. 116 // otherwise the text range may flicker to un-reversed for a frame, 117 // which can cause haptics and handles to be crossed. 118 selection = 119 mBuffer.selection.run { 120 takeUnless { mBufferState.selection.reversed } ?: TextRange(max, min) 121 }, 122 composition = mBuffer.composition 123 ) 124 125 mBufferState = newState 126 return newState 127 } 128 129 /** Returns the current state of the internal editing buffer as a [TextFieldValue]. */ toTextFieldValuenull130 fun toTextFieldValue(): TextFieldValue = mBufferState 131 132 private fun generateBatchErrorMessage( 133 editCommands: List<EditCommand>, 134 failedCommand: EditCommand?, 135 ): String = buildString { 136 appendLine( 137 "Error while applying EditCommand batch to buffer (" + 138 "length=${mBuffer.length}, " + 139 "composition=${mBuffer.composition}, " + 140 "selection=${mBuffer.selection}):" 141 ) 142 @Suppress("ListIterator") 143 editCommands.joinTo(this, separator = "\n") { 144 val prefix = if (failedCommand === it) " > " else " " 145 prefix + it.toStringForLog() 146 } 147 } 148 149 /** 150 * Generate a description of the command that is suitable for logging – this should not include 151 * any user-entered text, which may be sensitive. 152 */ toStringForLognull153 private fun EditCommand.toStringForLog(): String = 154 when (this) { 155 is CommitTextCommand -> 156 "CommitTextCommand(text.length=${text.length}, newCursorPosition=$newCursorPosition)" 157 is SetComposingTextCommand -> 158 "SetComposingTextCommand(text.length=${text.length}, " + 159 "newCursorPosition=$newCursorPosition)" 160 is SetComposingRegionCommand -> toString() 161 is DeleteSurroundingTextCommand -> toString() 162 is DeleteSurroundingTextInCodePointsCommand -> toString() 163 is SetSelectionCommand -> toString() 164 is FinishComposingTextCommand -> toString() 165 is BackspaceCommand -> toString() 166 is MoveCursorCommand -> toString() 167 is DeleteAllCommand -> toString() 168 // Do not return toString() by default, since that might contain sensitive text. 169 else -> "Unknown EditCommand: " + (this::class.simpleName ?: "{anonymous EditCommand}") 170 } 171 } 172