| 1 | // Copyright 2014 The Flutter Authors. All rights reserved. |
| 2 | // Use of this source code is governed by a BSD-style license that can be |
| 3 | // found in the LICENSE file. |
| 4 | |
| 5 | /// @docImport 'default_text_editing_shortcuts.dart'; |
| 6 | /// @docImport 'editable_text.dart'; |
| 7 | library; |
| 8 | |
| 9 | import 'package:flutter/services.dart'; |
| 10 | |
| 11 | import 'actions.dart'; |
| 12 | import 'basic.dart'; |
| 13 | import 'focus_manager.dart'; |
| 14 | |
| 15 | /// An [Intent] to send the event straight to the engine. |
| 16 | /// |
| 17 | /// See also: |
| 18 | /// |
| 19 | /// * [DefaultTextEditingShortcuts], which triggers this [Intent]. |
| 20 | class DoNothingAndStopPropagationTextIntent extends Intent { |
| 21 | /// Creates an instance of [DoNothingAndStopPropagationTextIntent]. |
| 22 | const DoNothingAndStopPropagationTextIntent(); |
| 23 | } |
| 24 | |
| 25 | /// A text editing related [Intent] that performs an operation towards a given |
| 26 | /// direction of the current caret location. |
| 27 | abstract class DirectionalTextEditingIntent extends Intent { |
| 28 | /// Creates a [DirectionalTextEditingIntent]. |
| 29 | const DirectionalTextEditingIntent(this.forward); |
| 30 | |
| 31 | /// Whether the input field, if applicable, should perform the text editing |
| 32 | /// operation from the current caret location towards the end of the document. |
| 33 | /// |
| 34 | /// Unless otherwise specified by the recipient of this intent, this parameter |
| 35 | /// uses the logical order of characters in the string to determine the |
| 36 | /// direction, and is not affected by the writing direction of the text. |
| 37 | final bool forward; |
| 38 | } |
| 39 | |
| 40 | /// Deletes the character before or after the caret location, based on whether |
| 41 | /// `forward` is true. |
| 42 | /// |
| 43 | /// {@template flutter.widgets.TextEditingIntents.logicalOrder} |
| 44 | /// {@endtemplate} |
| 45 | /// |
| 46 | /// Typically a text field will not respond to this intent if it has no active |
| 47 | /// caret ([TextSelection.isValid] is false for the current selection). |
| 48 | class DeleteCharacterIntent extends DirectionalTextEditingIntent { |
| 49 | /// Creates a [DeleteCharacterIntent]. |
| 50 | const DeleteCharacterIntent({required bool forward}) : super(forward); |
| 51 | } |
| 52 | |
| 53 | /// Deletes from the current caret location to the previous or next word |
| 54 | /// boundary, based on whether `forward` is true. |
| 55 | class DeleteToNextWordBoundaryIntent extends DirectionalTextEditingIntent { |
| 56 | /// Creates a [DeleteToNextWordBoundaryIntent]. |
| 57 | const DeleteToNextWordBoundaryIntent({required bool forward}) : super(forward); |
| 58 | } |
| 59 | |
| 60 | /// Deletes from the current caret location to the previous or next soft or hard |
| 61 | /// line break, based on whether `forward` is true. |
| 62 | class DeleteToLineBreakIntent extends DirectionalTextEditingIntent { |
| 63 | /// Creates a [DeleteToLineBreakIntent]. |
| 64 | const DeleteToLineBreakIntent({required bool forward}) : super(forward); |
| 65 | } |
| 66 | |
| 67 | /// A [DirectionalTextEditingIntent] that moves the caret or the selection to a |
| 68 | /// new location. |
| 69 | abstract class DirectionalCaretMovementIntent extends DirectionalTextEditingIntent { |
| 70 | /// Creates a [DirectionalCaretMovementIntent]. |
| 71 | const DirectionalCaretMovementIntent( |
| 72 | super.forward, |
| 73 | this.collapseSelection, [ |
| 74 | this.collapseAtReversal = false, |
| 75 | this.continuesAtWrap = false, |
| 76 | ]) : assert(!collapseSelection || !collapseAtReversal); |
| 77 | |
| 78 | /// Whether this [Intent] should make the selection collapsed (so it becomes a |
| 79 | /// caret), after the movement. |
| 80 | /// |
| 81 | /// When [collapseSelection] is false, the input field typically only moves |
| 82 | /// the current [TextSelection.extent] to the new location, while maintains |
| 83 | /// the current [TextSelection.base] location. |
| 84 | /// |
| 85 | /// When [collapseSelection] is true, the input field typically should move |
| 86 | /// both the [TextSelection.base] and the [TextSelection.extent] to the new |
| 87 | /// location. |
| 88 | final bool collapseSelection; |
| 89 | |
| 90 | /// Whether to collapse the selection when it would otherwise reverse order. |
| 91 | /// |
| 92 | /// For example, consider when forward is true and the extent is before the |
| 93 | /// base. If collapseAtReversal is true, then this will cause the selection to |
| 94 | /// collapse at the base. If it's false, then the extent will be placed at the |
| 95 | /// linebreak, reversing the order of base and offset. |
| 96 | /// |
| 97 | /// Cannot be true when collapseSelection is true. |
| 98 | final bool collapseAtReversal; |
| 99 | |
| 100 | /// Whether or not to continue to the next line at a wordwrap. |
| 101 | /// |
| 102 | /// If true, when an [Intent] to go to the beginning/end of a wordwrapped line |
| 103 | /// is received and the selection is already at the beginning/end of the line, |
| 104 | /// then the selection will be moved to the next/previous line. If false, the |
| 105 | /// selection will remain at the wordwrap. |
| 106 | final bool continuesAtWrap; |
| 107 | } |
| 108 | |
| 109 | /// Extends, or moves the current selection from the current |
| 110 | /// [TextSelection.extent] position to the previous or the next character |
| 111 | /// boundary. |
| 112 | class ExtendSelectionByCharacterIntent extends DirectionalCaretMovementIntent { |
| 113 | /// Creates an [ExtendSelectionByCharacterIntent]. |
| 114 | const ExtendSelectionByCharacterIntent({required bool forward, required bool collapseSelection}) |
| 115 | : super(forward, collapseSelection); |
| 116 | } |
| 117 | |
| 118 | /// Extends, or moves the current selection from the current |
| 119 | /// [TextSelection.extent] position to the previous or the next word |
| 120 | /// boundary. |
| 121 | class ExtendSelectionToNextWordBoundaryIntent extends DirectionalCaretMovementIntent { |
| 122 | /// Creates an [ExtendSelectionToNextWordBoundaryIntent]. |
| 123 | const ExtendSelectionToNextWordBoundaryIntent({ |
| 124 | required bool forward, |
| 125 | required bool collapseSelection, |
| 126 | }) : super(forward, collapseSelection); |
| 127 | } |
| 128 | |
| 129 | /// Extends, or moves the current selection from the current |
| 130 | /// [TextSelection.extent] position to the previous or the next word |
| 131 | /// boundary, or the [TextSelection.base] position if it's closer in the move |
| 132 | /// direction. |
| 133 | /// |
| 134 | /// This [Intent] typically has the same effect as an |
| 135 | /// [ExtendSelectionToNextWordBoundaryIntent], except it collapses the selection |
| 136 | /// when the order of [TextSelection.base] and [TextSelection.extent] would |
| 137 | /// reverse. |
| 138 | /// |
| 139 | /// This is typically only used on MacOS. |
| 140 | class ExtendSelectionToNextWordBoundaryOrCaretLocationIntent |
| 141 | extends DirectionalCaretMovementIntent { |
| 142 | /// Creates an [ExtendSelectionToNextWordBoundaryOrCaretLocationIntent]. |
| 143 | const ExtendSelectionToNextWordBoundaryOrCaretLocationIntent({required bool forward}) |
| 144 | : super(forward, false, true); |
| 145 | } |
| 146 | |
| 147 | /// Expands the current selection to the document boundary in the direction |
| 148 | /// given by [forward]. |
| 149 | /// |
| 150 | /// Unlike [ExpandSelectionToLineBreakIntent], the extent will be moved, which |
| 151 | /// matches the behavior on MacOS. |
| 152 | /// |
| 153 | /// See also: |
| 154 | /// |
| 155 | /// [ExtendSelectionToDocumentBoundaryIntent], which is similar but always |
| 156 | /// moves the extent. |
| 157 | class ExpandSelectionToDocumentBoundaryIntent extends DirectionalCaretMovementIntent { |
| 158 | /// Creates an [ExpandSelectionToDocumentBoundaryIntent]. |
| 159 | const ExpandSelectionToDocumentBoundaryIntent({required bool forward}) : super(forward, false); |
| 160 | } |
| 161 | |
| 162 | /// Expands the current selection to the closest line break in the direction |
| 163 | /// given by [forward]. |
| 164 | /// |
| 165 | /// Either the base or extent can move, whichever is closer to the line break. |
| 166 | /// The selection will never shrink. |
| 167 | /// |
| 168 | /// This behavior is common on MacOS. |
| 169 | /// |
| 170 | /// See also: |
| 171 | /// |
| 172 | /// [ExtendSelectionToLineBreakIntent], which is similar but always moves the |
| 173 | /// extent. |
| 174 | class ExpandSelectionToLineBreakIntent extends DirectionalCaretMovementIntent { |
| 175 | /// Creates an [ExpandSelectionToLineBreakIntent]. |
| 176 | const ExpandSelectionToLineBreakIntent({required bool forward}) : super(forward, false); |
| 177 | } |
| 178 | |
| 179 | /// Extends, or moves the current selection from the current |
| 180 | /// [TextSelection.extent] position to the closest line break in the direction |
| 181 | /// given by [forward]. |
| 182 | /// |
| 183 | /// See also: |
| 184 | /// |
| 185 | /// [ExpandSelectionToLineBreakIntent], which is similar but always increases |
| 186 | /// the size of the selection. |
| 187 | class ExtendSelectionToLineBreakIntent extends DirectionalCaretMovementIntent { |
| 188 | /// Creates an [ExtendSelectionToLineBreakIntent]. |
| 189 | const ExtendSelectionToLineBreakIntent({ |
| 190 | required bool forward, |
| 191 | required bool collapseSelection, |
| 192 | bool collapseAtReversal = false, |
| 193 | bool continuesAtWrap = false, |
| 194 | }) : assert(!collapseSelection || !collapseAtReversal), |
| 195 | super(forward, collapseSelection, collapseAtReversal, continuesAtWrap); |
| 196 | } |
| 197 | |
| 198 | /// Extends, or moves the current selection from the current |
| 199 | /// [TextSelection.extent] position to the closest position on the adjacent |
| 200 | /// line. |
| 201 | class ExtendSelectionVerticallyToAdjacentLineIntent extends DirectionalCaretMovementIntent { |
| 202 | /// Creates an [ExtendSelectionVerticallyToAdjacentLineIntent]. |
| 203 | const ExtendSelectionVerticallyToAdjacentLineIntent({ |
| 204 | required bool forward, |
| 205 | required bool collapseSelection, |
| 206 | }) : super(forward, collapseSelection); |
| 207 | } |
| 208 | |
| 209 | /// Expands, or moves the current selection from the current |
| 210 | /// [TextSelection.extent] position to the closest position on the adjacent |
| 211 | /// page. |
| 212 | class ExtendSelectionVerticallyToAdjacentPageIntent extends DirectionalCaretMovementIntent { |
| 213 | /// Creates an [ExtendSelectionVerticallyToAdjacentPageIntent]. |
| 214 | const ExtendSelectionVerticallyToAdjacentPageIntent({ |
| 215 | required bool forward, |
| 216 | required bool collapseSelection, |
| 217 | }) : super(forward, collapseSelection); |
| 218 | } |
| 219 | |
| 220 | /// Extends, or moves the current selection from the current |
| 221 | /// [TextSelection.extent] position to the previous or the next paragraph |
| 222 | /// boundary. |
| 223 | class ExtendSelectionToNextParagraphBoundaryIntent extends DirectionalCaretMovementIntent { |
| 224 | /// Creates an [ExtendSelectionToNextParagraphBoundaryIntent]. |
| 225 | const ExtendSelectionToNextParagraphBoundaryIntent({ |
| 226 | required bool forward, |
| 227 | required bool collapseSelection, |
| 228 | }) : super(forward, collapseSelection); |
| 229 | } |
| 230 | |
| 231 | /// Extends, or moves the current selection from the current |
| 232 | /// [TextSelection.extent] position to the previous or the next paragraph |
| 233 | /// boundary depending on the [forward] parameter. |
| 234 | /// |
| 235 | /// This [Intent] typically has the same effect as an |
| 236 | /// [ExtendSelectionToNextParagraphBoundaryIntent], except it collapses the selection |
| 237 | /// when the order of [TextSelection.base] and [TextSelection.extent] would |
| 238 | /// reverse. |
| 239 | /// |
| 240 | /// This is typically only used on MacOS. |
| 241 | class ExtendSelectionToNextParagraphBoundaryOrCaretLocationIntent |
| 242 | extends DirectionalCaretMovementIntent { |
| 243 | /// Creates an [ExtendSelectionToNextParagraphBoundaryOrCaretLocationIntent]. |
| 244 | const ExtendSelectionToNextParagraphBoundaryOrCaretLocationIntent({required bool forward}) |
| 245 | : super(forward, false, true); |
| 246 | } |
| 247 | |
| 248 | /// Extends, or moves the current selection from the current |
| 249 | /// [TextSelection.extent] position to the start or the end of the document. |
| 250 | /// |
| 251 | /// See also: |
| 252 | /// |
| 253 | /// [ExtendSelectionToDocumentBoundaryIntent], which is similar but always |
| 254 | /// increases the size of the selection. |
| 255 | class ExtendSelectionToDocumentBoundaryIntent extends DirectionalCaretMovementIntent { |
| 256 | /// Creates an [ExtendSelectionToDocumentBoundaryIntent]. |
| 257 | const ExtendSelectionToDocumentBoundaryIntent({ |
| 258 | required bool forward, |
| 259 | required bool collapseSelection, |
| 260 | }) : super(forward, collapseSelection); |
| 261 | } |
| 262 | |
| 263 | /// Scrolls to the beginning or end of the document depending on the [forward] |
| 264 | /// parameter. |
| 265 | class ScrollToDocumentBoundaryIntent extends DirectionalTextEditingIntent { |
| 266 | /// Creates a [ScrollToDocumentBoundaryIntent]. |
| 267 | const ScrollToDocumentBoundaryIntent({required bool forward}) : super(forward); |
| 268 | } |
| 269 | |
| 270 | /// Scrolls up or down by page depending on the [forward] parameter. |
| 271 | /// Extends the selection up or down by page based on the [forward] parameter. |
| 272 | class ExtendSelectionByPageIntent extends DirectionalTextEditingIntent { |
| 273 | /// Creates a [ExtendSelectionByPageIntent]. |
| 274 | const ExtendSelectionByPageIntent({required bool forward}) : super(forward); |
| 275 | } |
| 276 | |
| 277 | /// An [Intent] to select everything in the field. |
| 278 | class SelectAllTextIntent extends Intent { |
| 279 | /// Creates an instance of [SelectAllTextIntent]. |
| 280 | const SelectAllTextIntent(this.cause); |
| 281 | |
| 282 | /// {@template flutter.widgets.TextEditingIntents.cause} |
| 283 | /// The [SelectionChangedCause] that triggered the intent. |
| 284 | /// {@endtemplate} |
| 285 | final SelectionChangedCause cause; |
| 286 | } |
| 287 | |
| 288 | /// An [Intent] that represents a user interaction that attempts to copy or cut |
| 289 | /// the current selection in the field. |
| 290 | class CopySelectionTextIntent extends Intent { |
| 291 | const CopySelectionTextIntent._(this.cause, this.collapseSelection); |
| 292 | |
| 293 | /// Creates an [Intent] that represents a user interaction that attempts to |
| 294 | /// cut the current selection in the field. |
| 295 | const CopySelectionTextIntent.cut(SelectionChangedCause cause) : this._(cause, true); |
| 296 | |
| 297 | /// An [Intent] that represents a user interaction that attempts to copy the |
| 298 | /// current selection in the field. |
| 299 | static const CopySelectionTextIntent copy = CopySelectionTextIntent._( |
| 300 | SelectionChangedCause.keyboard, |
| 301 | false, |
| 302 | ); |
| 303 | |
| 304 | /// {@macro flutter.widgets.TextEditingIntents.cause} |
| 305 | final SelectionChangedCause cause; |
| 306 | |
| 307 | /// Whether the original text needs to be removed from the input field if the |
| 308 | /// copy action was successful. |
| 309 | final bool collapseSelection; |
| 310 | } |
| 311 | |
| 312 | /// An [Intent] to paste text from [Clipboard] to the field. |
| 313 | class PasteTextIntent extends Intent { |
| 314 | /// Creates an instance of [PasteTextIntent]. |
| 315 | const PasteTextIntent(this.cause); |
| 316 | |
| 317 | /// {@macro flutter.widgets.TextEditingIntents.cause} |
| 318 | final SelectionChangedCause cause; |
| 319 | } |
| 320 | |
| 321 | /// An [Intent] that represents a user interaction that attempts to go back to |
| 322 | /// the previous editing state. |
| 323 | class RedoTextIntent extends Intent { |
| 324 | /// Creates a [RedoTextIntent]. |
| 325 | const RedoTextIntent(this.cause); |
| 326 | |
| 327 | /// {@macro flutter.widgets.TextEditingIntents.cause} |
| 328 | final SelectionChangedCause cause; |
| 329 | } |
| 330 | |
| 331 | /// An [Intent] that represents a user interaction that attempts to modify the |
| 332 | /// current [TextEditingValue] in an input field. |
| 333 | class ReplaceTextIntent extends Intent { |
| 334 | /// Creates a [ReplaceTextIntent]. |
| 335 | const ReplaceTextIntent( |
| 336 | this.currentTextEditingValue, |
| 337 | this.replacementText, |
| 338 | this.replacementRange, |
| 339 | this.cause, |
| 340 | ); |
| 341 | |
| 342 | /// The [TextEditingValue] that this [Intent]'s action should perform on. |
| 343 | final TextEditingValue currentTextEditingValue; |
| 344 | |
| 345 | /// The text to replace the original text within the [replacementRange] with. |
| 346 | final String replacementText; |
| 347 | |
| 348 | /// The range of text in [currentTextEditingValue] that needs to be replaced. |
| 349 | final TextRange replacementRange; |
| 350 | |
| 351 | /// {@macro flutter.widgets.TextEditingIntents.cause} |
| 352 | final SelectionChangedCause cause; |
| 353 | } |
| 354 | |
| 355 | /// An [Intent] that represents a user interaction that attempts to go back to |
| 356 | /// the previous editing state. |
| 357 | class UndoTextIntent extends Intent { |
| 358 | /// Creates an [UndoTextIntent]. |
| 359 | const UndoTextIntent(this.cause); |
| 360 | |
| 361 | /// {@macro flutter.widgets.TextEditingIntents.cause} |
| 362 | final SelectionChangedCause cause; |
| 363 | } |
| 364 | |
| 365 | /// An [Intent] that represents a user interaction that attempts to change the |
| 366 | /// selection in an input field. |
| 367 | class UpdateSelectionIntent extends Intent { |
| 368 | /// Creates an [UpdateSelectionIntent]. |
| 369 | const UpdateSelectionIntent(this.currentTextEditingValue, this.newSelection, this.cause); |
| 370 | |
| 371 | /// The [TextEditingValue] that this [Intent]'s action should perform on. |
| 372 | final TextEditingValue currentTextEditingValue; |
| 373 | |
| 374 | /// The new [TextSelection] the input field should adopt. |
| 375 | final TextSelection newSelection; |
| 376 | |
| 377 | /// {@macro flutter.widgets.TextEditingIntents.cause} |
| 378 | final SelectionChangedCause cause; |
| 379 | } |
| 380 | |
| 381 | /// An [Intent] that represents a user interaction that attempts to swap the |
| 382 | /// characters immediately around the cursor. |
| 383 | class TransposeCharactersIntent extends Intent { |
| 384 | /// Creates a [TransposeCharactersIntent]. |
| 385 | const TransposeCharactersIntent(); |
| 386 | } |
| 387 | |
| 388 | /// An [Intent] that represents a tap outside the field. |
| 389 | /// |
| 390 | /// Invoked when the user taps outside the focused [EditableText] if |
| 391 | /// [EditableText.onTapOutside] is null. |
| 392 | /// |
| 393 | /// Override this [Intent] to modify the default behavior, which is to unfocus |
| 394 | /// on a touch event on web and do nothing on other platforms. |
| 395 | /// |
| 396 | /// See also: |
| 397 | /// |
| 398 | /// * [Action.overridable] for an example on how to make an [Action] |
| 399 | /// overridable. |
| 400 | class EditableTextTapOutsideIntent extends Intent { |
| 401 | /// Creates an [EditableTextTapOutsideIntent]. |
| 402 | const EditableTextTapOutsideIntent({required this.focusNode, required this.pointerDownEvent}); |
| 403 | |
| 404 | /// The [FocusNode] that this [Intent]'s action should be performed on. |
| 405 | final FocusNode focusNode; |
| 406 | |
| 407 | /// The [PointerDownEvent] that initiated this [Intent]. |
| 408 | final PointerDownEvent pointerDownEvent; |
| 409 | } |
| 410 | |
| 411 | /// An [Intent] that represents a tap outside the field. |
| 412 | /// |
| 413 | /// Invoked when the user taps up outside the focused [EditableText] if |
| 414 | /// [EditableText.onTapUpOutside] is null. |
| 415 | /// |
| 416 | /// Override this [Intent] to modify the default behavior, which is to unfocus |
| 417 | /// on a touch event on web and do nothing on other platforms. |
| 418 | /// |
| 419 | /// {@tool dartpad} |
| 420 | /// A common requirement is to unfocus text fields when the user taps outside of |
| 421 | /// it. For UX reasons, it's often desirable to only unfocus when the user taps |
| 422 | /// outside of the text field, but not when they scroll. |
| 423 | /// |
| 424 | /// To achieve this, you can override the default behavior of |
| 425 | /// [EditableTextTapOutsideIntent] and [EditableTextTapUpOutsideIntent] to check |
| 426 | /// the difference in distance between the pointer down and pointer up events |
| 427 | /// before potentially unfocusing. |
| 428 | /// |
| 429 | /// ** See code in examples/api/lib/widgets/text_editing_intents/editable_text_tap_up_outside_intent.0.dart ** |
| 430 | /// {@end-tool} |
| 431 | /// |
| 432 | /// See also: |
| 433 | /// |
| 434 | /// * [Action.overridable] for an example on how to make an [Action] |
| 435 | /// overridable. |
| 436 | class EditableTextTapUpOutsideIntent extends Intent { |
| 437 | /// Creates an [EditableTextTapUpOutsideIntent]. |
| 438 | const EditableTextTapUpOutsideIntent({required this.focusNode, required this.pointerUpEvent}); |
| 439 | |
| 440 | /// The [FocusNode] that this [Intent]'s action should be performed on. |
| 441 | final FocusNode focusNode; |
| 442 | |
| 443 | /// The [PointerUpEvent] that initiated this [Intent]. |
| 444 | final PointerUpEvent pointerUpEvent; |
| 445 | } |
| 446 | |