1 /*
2  * Copyright 2021 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 package androidx.compose.foundation.text
18 
19 import androidx.compose.foundation.text.selection.BaseTextPreparedSelection.Companion.NoCharacterFound
20 import androidx.compose.foundation.text.selection.TextFieldPreparedSelection
21 import androidx.compose.foundation.text.selection.TextFieldSelectionManager
22 import androidx.compose.foundation.text.selection.TextPreparedSelectionState
23 import androidx.compose.runtime.remember
24 import androidx.compose.ui.Modifier
25 import androidx.compose.ui.composed
26 import androidx.compose.ui.input.key.KeyEvent
27 import androidx.compose.ui.input.key.KeyEventType
28 import androidx.compose.ui.input.key.onKeyEvent
29 import androidx.compose.ui.input.key.type
30 import androidx.compose.ui.text.input.CommitTextCommand
31 import androidx.compose.ui.text.input.DeleteSurroundingTextCommand
32 import androidx.compose.ui.text.input.EditCommand
33 import androidx.compose.ui.text.input.FinishComposingTextCommand
34 import androidx.compose.ui.text.input.ImeAction
35 import androidx.compose.ui.text.input.OffsetMapping
36 import androidx.compose.ui.text.input.TextFieldValue
37 
38 // AWT and Android have similar but different key event models. In android there are two main
39 // types of events: ACTION_DOWN and ACTION_UP. In AWT there is additional KEY_TYPED which should
40 // be used to get "typed character". By this simple function we are introducing common
41 // denominator for both systems: if KeyEvent.isTypedEvent then it's safe to use
42 // KeyEvent.utf16CodePoint
43 internal expect val KeyEvent.isTypedEvent: Boolean
44 
45 /**
46  * It handles [KeyEvent]s and either process them as typed events or maps to [KeyCommand] via
47  * [KeyMapping]. [KeyCommand] then is executed using utility class [TextFieldPreparedSelection]
48  */
49 internal class TextFieldKeyInput(
50     val state: LegacyTextFieldState,
51     val selectionManager: TextFieldSelectionManager,
52     val value: TextFieldValue = TextFieldValue(),
53     val editable: Boolean = true,
54     val singleLine: Boolean = false,
55     val preparedSelectionState: TextPreparedSelectionState,
56     val offsetMapping: OffsetMapping = OffsetMapping.Identity,
57     val undoManager: UndoManager? = null,
58     private val keyCombiner: DeadKeyCombiner,
59     private val keyMapping: KeyMapping = platformDefaultKeyMapping,
<lambda>null60     private val onValueChange: (TextFieldValue) -> Unit = {},
61     private val imeAction: ImeAction,
62 ) {
applynull63     private fun List<EditCommand>.apply() {
64         val newTextFieldValue =
65             state.processor.apply(
66                 this.toMutableList().apply { add(0, FinishComposingTextCommand()) }
67             )
68 
69         onValueChange(newTextFieldValue)
70     }
71 
applynull72     private fun EditCommand.apply() {
73         listOf(this).apply()
74     }
75 
typedCommandnull76     private fun typedCommand(event: KeyEvent): CommitTextCommand? {
77         if (!event.isTypedEvent) {
78             return null
79         }
80 
81         val codePoint = keyCombiner.consume(event) ?: return null
82         val text = StringBuilder().appendCodePointX(codePoint).toString()
83         return CommitTextCommand(text, 1)
84     }
85 
processnull86     fun process(event: KeyEvent): Boolean {
87         typedCommand(event)?.let {
88             return if (editable) {
89                 it.apply()
90                 preparedSelectionState.resetCachedX()
91                 true
92             } else {
93                 false
94             }
95         }
96         if (event.type != KeyEventType.KeyDown) {
97             return false
98         }
99         val command = keyMapping.map(event)
100         if (command == null || (command.editsText && !editable)) {
101             return false
102         }
103         var consumed = true
104         commandExecutionContext {
105             when (command) {
106                 KeyCommand.COPY -> selectionManager.copy(false)
107                 // TODO(siyamed): cut & paste will cause a reset input
108                 KeyCommand.PASTE -> selectionManager.paste()
109                 KeyCommand.CUT -> selectionManager.cut()
110                 KeyCommand.LEFT_CHAR -> collapseLeftOr { moveCursorLeft() }
111                 KeyCommand.RIGHT_CHAR -> collapseRightOr { moveCursorRight() }
112                 KeyCommand.LEFT_WORD -> moveCursorLeftByWord()
113                 KeyCommand.RIGHT_WORD -> moveCursorRightByWord()
114                 KeyCommand.PREV_PARAGRAPH -> moveCursorPrevByParagraph()
115                 KeyCommand.NEXT_PARAGRAPH -> moveCursorNextByParagraph()
116                 KeyCommand.UP -> moveCursorUpByLine()
117                 KeyCommand.DOWN -> moveCursorDownByLine()
118                 KeyCommand.PAGE_UP -> moveCursorUpByPage()
119                 KeyCommand.PAGE_DOWN -> moveCursorDownByPage()
120                 KeyCommand.LINE_START -> moveCursorToLineStart()
121                 KeyCommand.LINE_END -> moveCursorToLineEnd()
122                 KeyCommand.LINE_LEFT -> moveCursorToLineLeftSide()
123                 KeyCommand.LINE_RIGHT -> moveCursorToLineRightSide()
124                 KeyCommand.HOME -> moveCursorToHome()
125                 KeyCommand.END -> moveCursorToEnd()
126                 KeyCommand.DELETE_PREV_CHAR ->
127                     deleteIfSelectedOr {
128                             val precedingCodePointIndex = getPrecedingCodePointOrEmojiStartIndex()
129                             if (precedingCodePointIndex == NoCharacterFound) {
130                                 return@deleteIfSelectedOr null
131                             }
132                             DeleteSurroundingTextCommand(selection.end - precedingCodePointIndex, 0)
133                         }
134                         ?.apply()
135                 KeyCommand.DELETE_NEXT_CHAR -> {
136                     // Note that some software keyboards, such as Samsungs, go through this code
137                     // path instead of making calls on the InputConnection directly.
138                     deleteIfSelectedOr {
139                             val nextCharacterIndex = getNextCharacterIndex()
140                             // If there's no next character, it means the cursor is at the end of
141                             // the
142                             // text, and this should be a no-op. See b/199919707.
143                             if (nextCharacterIndex != NoCharacterFound) {
144                                 DeleteSurroundingTextCommand(0, nextCharacterIndex - selection.end)
145                             } else {
146                                 null
147                             }
148                         }
149                         ?.apply()
150                 }
151                 KeyCommand.DELETE_PREV_WORD ->
152                     deleteIfSelectedOr {
153                             getPreviousWordOffset()?.let {
154                                 DeleteSurroundingTextCommand(selection.end - it, 0)
155                             }
156                         }
157                         ?.apply()
158                 KeyCommand.DELETE_NEXT_WORD ->
159                     deleteIfSelectedOr {
160                             getNextWordOffset()?.let {
161                                 DeleteSurroundingTextCommand(0, it - selection.end)
162                             }
163                         }
164                         ?.apply()
165                 KeyCommand.DELETE_FROM_LINE_START ->
166                     deleteIfSelectedOr {
167                             getLineStartByOffset()?.let {
168                                 DeleteSurroundingTextCommand(selection.end - it, 0)
169                             }
170                         }
171                         ?.apply()
172                 KeyCommand.DELETE_TO_LINE_END ->
173                     deleteIfSelectedOr {
174                             getLineEndByOffset()?.let {
175                                 DeleteSurroundingTextCommand(0, it - selection.end)
176                             }
177                         }
178                         ?.apply()
179                 KeyCommand.NEW_LINE ->
180                     if (!singleLine) {
181                         CommitTextCommand("\n", 1).apply()
182                     } else {
183                         consumed =
184                             this@TextFieldKeyInput.state.onImeActionPerformedWithResult(imeAction)
185                     }
186                 KeyCommand.TAB ->
187                     if (!singleLine) {
188                         CommitTextCommand("\t", 1).apply()
189                     } else {
190                         consumed = false // let propagate to focus system
191                     }
192                 KeyCommand.SELECT_ALL -> selectAll()
193                 KeyCommand.SELECT_LEFT_CHAR -> moveCursorLeft().selectMovement()
194                 KeyCommand.SELECT_RIGHT_CHAR -> moveCursorRight().selectMovement()
195                 KeyCommand.SELECT_LEFT_WORD -> moveCursorLeftByWord().selectMovement()
196                 KeyCommand.SELECT_RIGHT_WORD -> moveCursorRightByWord().selectMovement()
197                 KeyCommand.SELECT_PREV_PARAGRAPH -> moveCursorPrevByParagraph().selectMovement()
198                 KeyCommand.SELECT_NEXT_PARAGRAPH -> moveCursorNextByParagraph().selectMovement()
199                 KeyCommand.SELECT_LINE_START -> moveCursorToLineStart().selectMovement()
200                 KeyCommand.SELECT_LINE_END -> moveCursorToLineEnd().selectMovement()
201                 KeyCommand.SELECT_LINE_LEFT -> moveCursorToLineLeftSide().selectMovement()
202                 KeyCommand.SELECT_LINE_RIGHT -> moveCursorToLineRightSide().selectMovement()
203                 KeyCommand.SELECT_UP -> moveCursorUpByLine().selectMovement()
204                 KeyCommand.SELECT_DOWN -> moveCursorDownByLine().selectMovement()
205                 KeyCommand.SELECT_PAGE_UP -> moveCursorUpByPage().selectMovement()
206                 KeyCommand.SELECT_PAGE_DOWN -> moveCursorDownByPage().selectMovement()
207                 KeyCommand.SELECT_HOME -> moveCursorToHome().selectMovement()
208                 KeyCommand.SELECT_END -> moveCursorToEnd().selectMovement()
209                 KeyCommand.DESELECT -> deselect()
210                 KeyCommand.UNDO -> {
211                     undoManager?.makeSnapshot(value)
212                     undoManager?.undo()?.let { this@TextFieldKeyInput.onValueChange(it) }
213                 }
214                 KeyCommand.REDO -> {
215                     undoManager?.redo()?.let { this@TextFieldKeyInput.onValueChange(it) }
216                 }
217                 KeyCommand.CHARACTER_PALETTE -> {
218                     showCharacterPalette()
219                 }
220             }
221         }
222         undoManager?.forceNextSnapshot()
223         return consumed
224     }
225 
commandExecutionContextnull226     private fun commandExecutionContext(block: TextFieldPreparedSelection.() -> Unit) {
227         val preparedSelection =
228             TextFieldPreparedSelection(
229                 currentValue = value,
230                 offsetMapping = offsetMapping,
231                 layoutResultProxy = state.layoutResult,
232                 state = preparedSelectionState
233             )
234         block(preparedSelection)
235         if (
236             preparedSelection.selection != value.selection ||
237                 preparedSelection.annotatedString != value.annotatedString
238         ) {
239             onValueChange(preparedSelection.value)
240         }
241     }
242 }
243 
textFieldKeyInputnull244 internal fun Modifier.textFieldKeyInput(
245     state: LegacyTextFieldState,
246     manager: TextFieldSelectionManager,
247     value: TextFieldValue,
248     onValueChange: (TextFieldValue) -> Unit = {},
249     editable: Boolean,
250     singleLine: Boolean,
251     offsetMapping: OffsetMapping,
252     undoManager: UndoManager,
253     imeAction: ImeAction,
<lambda>null254 ) = composed {
255     val preparedSelectionState = remember { TextPreparedSelectionState() }
256     val keyCombiner = remember { DeadKeyCombiner() }
257     val processor =
258         TextFieldKeyInput(
259             state = state,
260             selectionManager = manager,
261             value = value,
262             editable = editable,
263             singleLine = singleLine,
264             offsetMapping = offsetMapping,
265             preparedSelectionState = preparedSelectionState,
266             undoManager = undoManager,
267             keyCombiner = keyCombiner,
268             onValueChange = onValueChange,
269             imeAction = imeAction,
270         )
271     Modifier.onKeyEvent(processor::process)
272 }
273