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