1 /* <lambda>null2 * 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.annotation.RestrictTo 22 import androidx.compose.ui.geometry.Rect 23 import androidx.compose.ui.graphics.Matrix 24 import androidx.compose.ui.text.AtomicReference 25 import androidx.compose.ui.text.InternalTextApi 26 import androidx.compose.ui.text.TextLayoutResult 27 28 /** 29 * Handles communication with the IME. Informs about the IME changes via [EditCommand]s and provides 30 * utilities for working with software keyboard. 31 * 32 * This class is responsible for ensuring there is only one open [TextInputSession] which will 33 * interact with software keyboards. Start new a TextInputSession by calling [startInput] and close 34 * it with [stopInput]. 35 */ 36 // Open for testing purposes. 37 @Deprecated("Use PlatformTextInputModifierNode instead.") 38 open class TextInputService(private val platformTextInputService: PlatformTextInputService) { 39 private val _currentInputSession: AtomicReference<TextInputSession?> = AtomicReference(null) 40 41 internal val currentInputSession: TextInputSession? 42 get() = _currentInputSession.get() 43 44 /** 45 * Start text input session for given client. 46 * 47 * If there is a previous [TextInputSession] open, it will immediately be closed by this call to 48 * [startInput]. 49 * 50 * @param value initial [TextFieldValue] 51 * @param imeOptions IME configuration 52 * @param onEditCommand callback to inform about changes requested by IME 53 * @param onImeActionPerformed callback to inform if an IME action such as [ImeAction.Done] etc 54 * occurred. 55 */ 56 open fun startInput( 57 value: TextFieldValue, 58 imeOptions: ImeOptions, 59 onEditCommand: (List<EditCommand>) -> Unit, 60 onImeActionPerformed: (ImeAction) -> Unit 61 ): TextInputSession { 62 platformTextInputService.startInput(value, imeOptions, onEditCommand, onImeActionPerformed) 63 val nextSession = TextInputSession(this, platformTextInputService) 64 _currentInputSession.set(nextSession) 65 return nextSession 66 } 67 68 /** 69 * Restart input and show the keyboard. This should only be called when starting a new 70 * `PlatformTextInputModifierNode.textInputSession`. 71 */ 72 @InternalTextApi 73 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) 74 fun startInput() { 75 platformTextInputService.startInput() 76 val nextSession = TextInputSession(this, platformTextInputService) 77 _currentInputSession.set(nextSession) 78 } 79 80 /** 81 * Stop text input session. 82 * 83 * If the [session] is not the currently open session, no action will occur. 84 * 85 * @param session the session returned by [startInput] call. 86 */ 87 open fun stopInput(session: TextInputSession) { 88 if (_currentInputSession.compareAndSet(session, null)) { 89 platformTextInputService.stopInput() 90 } 91 } 92 93 @InternalTextApi 94 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) 95 fun stopInput() { 96 // This is a direct stop call, there's no need to compare the current input session. 97 _currentInputSession.set(null) 98 platformTextInputService.stopInput() 99 } 100 101 /** 102 * Request showing onscreen keyboard. 103 * 104 * This call will be ignored if there is not an open [TextInputSession], as it means there is 105 * nothing that will accept typed input. The most common way to open a TextInputSession is to 106 * set the focus to an editable text composable. 107 * 108 * There is no guarantee that the keyboard will be shown. The software keyboard or system 109 * service may silently ignore this request. 110 */ 111 @Deprecated( 112 message = 113 "Use SoftwareKeyboardController.show or " + 114 "TextInputSession.showSoftwareKeyboard instead.", 115 replaceWith = ReplaceWith("textInputSession.showSoftwareKeyboard()") 116 ) 117 // TODO(b/183448615) @InternalTextApi 118 fun showSoftwareKeyboard() { 119 if (currentInputSession != null) { 120 platformTextInputService.showSoftwareKeyboard() 121 } 122 } 123 124 /** Hide onscreen keyboard. */ 125 @Deprecated( 126 message = 127 "Use SoftwareKeyboardController.hide or " + 128 "TextInputSession.hideSoftwareKeyboard instead.", 129 replaceWith = ReplaceWith("textInputSession.hideSoftwareKeyboard()") 130 ) 131 // TODO(b/183448615) @InternalTextApi 132 fun hideSoftwareKeyboard(): Unit = platformTextInputService.hideSoftwareKeyboard() 133 } 134 135 /** 136 * Represents a input session for interactions between a soft keyboard and editable text. 137 * 138 * This session may be closed at any time by [TextInputService] or by calling [dispose], after which 139 * [isOpen] will return false and all further calls will have no effect. 140 */ 141 @Deprecated("Use PlatformTextInputModifierNode instead.") 142 class TextInputSession( 143 private val textInputService: TextInputService, 144 private val platformTextInputService: PlatformTextInputService 145 ) { 146 /** 147 * If this session is currently open. 148 * 149 * A session may be closed at any time by [TextInputService] or by calling [dispose]. 150 */ 151 val isOpen: Boolean 152 get() = textInputService.currentInputSession == this 153 154 /** 155 * Close this input session. 156 * 157 * All further calls to this object will have no effect, and [isOpen] will return false. 158 * 159 * Note, [TextInputService] may also close this input session at any time without calling 160 * dispose. Calling dispose after this session has been closed has no effect. 161 */ disposenull162 fun dispose() { 163 textInputService.stopInput(this) 164 } 165 166 /** 167 * Execute [block] if [isOpen] is true. 168 * 169 * This function will only check [isOpen] once, and may execute the action after the input 170 * session closes in the case of concurrent execution. 171 * 172 * @param block action to take if isOpen 173 * @return true if an action was performed 174 */ ensureOpenSessionnull175 private inline fun ensureOpenSession(block: () -> Unit): Boolean { 176 return isOpen.also { applying -> 177 if (applying) { 178 block() 179 } 180 } 181 } 182 183 /** 184 * Notify the focused rectangle to the system. 185 * 186 * The system can ignore this information or use it to for additional functionality. 187 * 188 * For example, desktop systems show a popup near the focused input area (for some languages). 189 * 190 * If the session is not open, no action will be performed. 191 * 192 * @param rect the rectangle that describes the boundaries on the screen that requires focus 193 * @return false if this session expired and no action was performed 194 */ <lambda>null195 fun notifyFocusedRect(rect: Rect): Boolean = ensureOpenSession { 196 platformTextInputService.notifyFocusedRect(rect) 197 } 198 199 /** 200 * Notify the input service of layout and position changes. 201 * 202 * @param textFieldValue the text field's [TextFieldValue] 203 * @param offsetMapping the offset mapping for the visual transformation 204 * @param textLayoutResult the text field's [TextLayoutResult] 205 * @param textFieldToRootTransform function that modifies a matrix to be a transformation matrix 206 * from local coordinates to the root composable coordinates 207 * @param innerTextFieldBounds visible bounds of the text field in text layout coordinates, or 208 * an empty rectangle if the text field is not visible 209 * @param decorationBoxBounds visible bounds of the decoration box in text layout coordinates, 210 * or an empty rectangle if the decoration box is not visible 211 */ updateTextLayoutResultnull212 fun updateTextLayoutResult( 213 textFieldValue: TextFieldValue, 214 offsetMapping: OffsetMapping, 215 textLayoutResult: TextLayoutResult, 216 textFieldToRootTransform: (Matrix) -> Unit, 217 innerTextFieldBounds: Rect, 218 decorationBoxBounds: Rect 219 ) = ensureOpenSession { 220 platformTextInputService.updateTextLayoutResult( 221 textFieldValue, 222 offsetMapping, 223 textLayoutResult, 224 textFieldToRootTransform, 225 innerTextFieldBounds, 226 decorationBoxBounds 227 ) 228 } 229 230 /** 231 * Notify IME about the new [TextFieldValue] and latest state of the editing buffer. [oldValue] 232 * is the state of the buffer before the changes applied by the [newValue]. 233 * 234 * [oldValue] represents the changes that was requested by IME on the buffer, and [newValue] is 235 * the final state of the editing buffer that was requested by the application. In cases where 236 * [oldValue] is not equal to [newValue], it would mean the IME suggested value is rejected, and 237 * the IME connection will be restarted with the newValue. 238 * 239 * If the session is not open, action will be performed. 240 * 241 * @param oldValue the value that was requested by IME on the buffer 242 * @param newValue final state of the editing buffer that was requested by the application 243 * @return false if this session expired and no action was performed 244 */ updateStatenull245 fun updateState(oldValue: TextFieldValue?, newValue: TextFieldValue): Boolean = 246 ensureOpenSession { 247 platformTextInputService.updateState(oldValue, newValue) 248 } 249 250 /** 251 * Request showing onscreen keyboard. 252 * 253 * This call will have no effect if this session is not open. 254 * 255 * This should be used instead of [TextInputService.showSoftwareKeyboard] when implementing a 256 * new editable text composable to show the keyboard in response to events related to that 257 * composable. 258 * 259 * There is no guarantee that the keyboard will be shown. The software keyboard or system 260 * service may silently ignore this request. 261 * 262 * @return false if this session expired and no action was performed 263 */ 264 // TODO(b/241399013) Deprecate when out of API freeze. 265 // @Deprecated("Use SoftwareKeyboardController.show() instead.") <lambda>null266 fun showSoftwareKeyboard(): Boolean = ensureOpenSession { 267 platformTextInputService.showSoftwareKeyboard() 268 } 269 270 /** 271 * Hide onscreen keyboard for a specific [TextInputSession]. 272 * 273 * This call will have no effect if this session is not open. 274 * 275 * This should be used instead of [TextInputService.showSoftwareKeyboard] when implementing a 276 * new editable text composable to hide the keyboard in response to events related to that 277 * composable. 278 * 279 * @return false if this session expired and no action was performed 280 */ 281 // TODO(b/241399013) Deprecate when out of API freeze. 282 // @Deprecated("Use SoftwareKeyboardController.hide() instead.") <lambda>null283 fun hideSoftwareKeyboard(): Boolean = ensureOpenSession { 284 platformTextInputService.hideSoftwareKeyboard() 285 } 286 } 287 288 /** Platform specific text input service. */ 289 @Deprecated("Use PlatformTextInputModifierNode instead.") 290 interface PlatformTextInputService { 291 /** 292 * Start text input session for given client. 293 * 294 * @see TextInputService.startInput 295 */ startInputnull296 fun startInput( 297 value: TextFieldValue, 298 imeOptions: ImeOptions, 299 onEditCommand: (List<EditCommand>) -> Unit, 300 onImeActionPerformed: (ImeAction) -> Unit 301 ) 302 303 /** 304 * Restart input and show the keyboard. This should only be called when starting a new 305 * `PlatformTextInputModifierNode.textInputSession`. 306 * 307 * @see TextInputService.startInput 308 */ 309 fun startInput() {} 310 311 /** 312 * Stop text input session. 313 * 314 * @see TextInputService.stopInput 315 */ stopInputnull316 fun stopInput() 317 318 /** 319 * Request showing onscreen keyboard 320 * 321 * There is no guarantee nor callback of the result of this API. 322 * 323 * @see TextInputService.showSoftwareKeyboard 324 */ 325 fun showSoftwareKeyboard() 326 327 /** 328 * Hide software keyboard 329 * 330 * @see TextInputService.hideSoftwareKeyboard 331 */ 332 fun hideSoftwareKeyboard() 333 334 /** 335 * Notify the new editor model to IME. 336 * 337 * @see TextInputSession.updateState 338 */ 339 fun updateState(oldValue: TextFieldValue?, newValue: TextFieldValue) 340 341 /** 342 * Notify the focused rectangle to the system. 343 * 344 * The system can ignore this information or use it to for additional functionality. 345 * 346 * For example, desktop systems show a popup near the focused input area (for some languages). 347 */ 348 // TODO(b/262648050) Try to find a better API. 349 fun notifyFocusedRect(rect: Rect) {} 350 351 /** 352 * Notify the input service of layout and position changes. 353 * 354 * @see TextInputSession.updateTextLayoutResult 355 */ updateTextLayoutResultnull356 fun updateTextLayoutResult( 357 textFieldValue: TextFieldValue, 358 offsetMapping: OffsetMapping, 359 textLayoutResult: TextLayoutResult, 360 textFieldToRootTransform: (Matrix) -> Unit, 361 innerTextFieldBounds: Rect, 362 decorationBoxBounds: Rect 363 ) {} 364 } 365