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 'package:flutter/cupertino.dart'; |
6 | /// @docImport 'package:flutter/material.dart'; |
7 | /// |
8 | /// @docImport 'app.dart'; |
9 | /// @docImport 'context_menu_controller.dart'; |
10 | /// @docImport 'form.dart'; |
11 | /// @docImport 'restoration.dart'; |
12 | /// @docImport 'restoration_properties.dart'; |
13 | /// @docImport 'selectable_region.dart'; |
14 | /// @docImport 'text_selection_toolbar_layout_delegate.dart'; |
15 | library; |
16 | |
17 | import 'dart:async'; |
18 | import 'dart:math' as math; |
19 | import 'dart:ui' as ui hide TextStyle; |
20 | |
21 | import 'package:characters/characters.dart'show CharacterRange, StringCharacters; |
22 | import 'package:flutter/foundation.dart'; |
23 | import 'package:flutter/gestures.dart'; |
24 | import 'package:flutter/rendering.dart'; |
25 | import 'package:flutter/scheduler.dart'; |
26 | import 'package:flutter/services.dart'; |
27 | |
28 | import 'actions.dart'; |
29 | import 'app_lifecycle_listener.dart'; |
30 | import 'autofill.dart'; |
31 | import 'automatic_keep_alive.dart'; |
32 | import 'basic.dart'; |
33 | import 'binding.dart'; |
34 | import 'constants.dart'; |
35 | import 'context_menu_button_item.dart'; |
36 | import 'debug.dart'; |
37 | import 'default_selection_style.dart'; |
38 | import 'default_text_editing_shortcuts.dart'; |
39 | import 'focus_manager.dart'; |
40 | import 'focus_scope.dart'; |
41 | import 'focus_traversal.dart'; |
42 | import 'framework.dart'; |
43 | import 'localizations.dart'; |
44 | import 'magnifier.dart'; |
45 | import 'media_query.dart'; |
46 | import 'notification_listener.dart'; |
47 | import 'scroll_configuration.dart'; |
48 | import 'scroll_controller.dart'; |
49 | import 'scroll_notification.dart'; |
50 | import 'scroll_notification_observer.dart'; |
51 | import 'scroll_physics.dart'; |
52 | import 'scroll_position.dart'; |
53 | import 'scrollable.dart'; |
54 | import 'scrollable_helpers.dart'; |
55 | import 'shortcuts.dart'; |
56 | import 'size_changed_layout_notifier.dart'; |
57 | import 'spell_check.dart'; |
58 | import 'tap_region.dart'; |
59 | import 'text.dart'; |
60 | import 'text_editing_intents.dart'; |
61 | import 'text_selection.dart'; |
62 | import 'text_selection_toolbar_anchors.dart'; |
63 | import 'ticker_provider.dart'; |
64 | import 'undo_history.dart'; |
65 | import 'view.dart'; |
66 | import 'widget_span.dart'; |
67 | |
68 | export 'package:flutter/services.dart' |
69 | show |
70 | KeyboardInsertedContent, |
71 | SelectionChangedCause, |
72 | SmartDashesType, |
73 | SmartQuotesType, |
74 | TextEditingValue, |
75 | TextInputType, |
76 | TextSelection; |
77 | |
78 | // Examples can assume: |
79 | // late BuildContext context; |
80 | // late WidgetTester tester; |
81 | |
82 | /// Signature for the callback that reports when the user changes the selection |
83 | /// (including the cursor location). |
84 | typedef SelectionChangedCallback = |
85 | void Function(TextSelection selection, SelectionChangedCause? cause); |
86 | |
87 | /// Signature for the callback that reports the app private command results. |
88 | typedef AppPrivateCommandCallback = void Function(String action, Map<String, dynamic> data); |
89 | |
90 | /// Signature for a widget builder that builds a context menu for the given |
91 | /// [EditableTextState]. |
92 | /// |
93 | /// See also: |
94 | /// |
95 | /// * [SelectableRegionContextMenuBuilder], which performs the same role for |
96 | /// [SelectableRegion]. |
97 | typedef EditableTextContextMenuBuilder = |
98 | Widget Function(BuildContext context, EditableTextState editableTextState); |
99 | |
100 | // Signature for a function that determines the target location of the given |
101 | // [TextPosition] after applying the given [TextBoundary]. |
102 | typedef _ApplyTextBoundary = TextPosition Function(TextPosition, bool, TextBoundary); |
103 | |
104 | // The time it takes for the cursor to fade from fully opaque to fully |
105 | // transparent and vice versa. A full cursor blink, from transparent to opaque |
106 | // to transparent, is twice this duration. |
107 | const Duration _kCursorBlinkHalfPeriod = Duration(milliseconds: 500); |
108 | |
109 | // Number of cursor ticks during which the most recently entered character |
110 | // is shown in an obscured text field. |
111 | const int _kObscureShowLatestCharCursorTicks = 3; |
112 | |
113 | /// The default mime types to be used when allowedMimeTypes is not provided. |
114 | /// |
115 | /// The default value supports inserting images of any supported format. |
116 | const List<String> kDefaultContentInsertionMimeTypes = <String>[ |
117 | 'image/png', |
118 | 'image/bmp', |
119 | 'image/jpg', |
120 | 'image/tiff', |
121 | 'image/gif', |
122 | 'image/jpeg', |
123 | 'image/webp', |
124 | ]; |
125 | |
126 | class _CompositionCallback extends SingleChildRenderObjectWidget { |
127 | const _CompositionCallback({required this.compositeCallback, required this.enabled, super.child}); |
128 | final CompositionCallback compositeCallback; |
129 | final bool enabled; |
130 | |
131 | @override |
132 | RenderObject createRenderObject(BuildContext context) { |
133 | return _RenderCompositionCallback(compositeCallback, enabled); |
134 | } |
135 | |
136 | @override |
137 | void updateRenderObject(BuildContext context, _RenderCompositionCallback renderObject) { |
138 | super.updateRenderObject(context, renderObject); |
139 | // _EditableTextState always uses the same callback. |
140 | assert(renderObject.compositeCallback == compositeCallback); |
141 | renderObject.enabled = enabled; |
142 | } |
143 | } |
144 | |
145 | class _RenderCompositionCallback extends RenderProxyBox { |
146 | _RenderCompositionCallback(this.compositeCallback, this._enabled); |
147 | |
148 | final CompositionCallback compositeCallback; |
149 | VoidCallback? _cancelCallback; |
150 | |
151 | bool get enabled => _enabled; |
152 | bool _enabled = false; |
153 | set enabled(bool newValue) { |
154 | _enabled = newValue; |
155 | if (!newValue) { |
156 | _cancelCallback?.call(); |
157 | _cancelCallback = null; |
158 | } else if (_cancelCallback == null) { |
159 | markNeedsPaint(); |
160 | } |
161 | } |
162 | |
163 | @override |
164 | void paint(PaintingContext context, ui.Offset offset) { |
165 | if (enabled) { |
166 | _cancelCallback ??= context.addCompositionCallback(compositeCallback); |
167 | } |
168 | super.paint(context, offset); |
169 | } |
170 | } |
171 | |
172 | /// A controller for an editable text field. |
173 | /// |
174 | /// Whenever the user modifies a text field with an associated |
175 | /// [TextEditingController], the text field updates [value] and the controller |
176 | /// notifies its listeners. Listeners can then read the [text] and [selection] |
177 | /// properties to learn what the user has typed or how the selection has been |
178 | /// updated. |
179 | /// |
180 | /// Similarly, if you modify the [text] or [selection] properties, the text |
181 | /// field will be notified and will update itself appropriately. |
182 | /// |
183 | /// A [TextEditingController] can also be used to provide an initial value for a |
184 | /// text field. If you build a text field with a controller that already has |
185 | /// [text], the text field will use that text as its initial value. |
186 | /// |
187 | /// The [value] (as well as [text] and [selection]) of this controller can be |
188 | /// updated from within a listener added to this controller. Be aware of |
189 | /// infinite loops since the listener will also be notified of the changes made |
190 | /// from within itself. Modifying the composing region from within a listener |
191 | /// can also have a bad interaction with some input methods. Gboard, for |
192 | /// example, will try to restore the composing region of the text if it was |
193 | /// modified programmatically, creating an infinite loop of communications |
194 | /// between the framework and the input method. Consider using |
195 | /// [TextInputFormatter]s instead for as-you-type text modification. |
196 | /// |
197 | /// If both the [text] and [selection] properties need to be changed, set the |
198 | /// controller's [value] instead. Setting [text] will clear the selection |
199 | /// and composing range. |
200 | /// |
201 | /// Remember to [dispose] of the [TextEditingController] when it is no longer |
202 | /// needed. This will ensure we discard any resources used by the object. |
203 | /// |
204 | /// {@tool dartpad} |
205 | /// This example creates a [TextField] with a [TextEditingController] whose |
206 | /// change listener forces the entered text to be lower case and keeps the |
207 | /// cursor at the end of the input. |
208 | /// |
209 | /// ** See code in examples/api/lib/widgets/editable_text/text_editing_controller.0.dart ** |
210 | /// {@end-tool} |
211 | /// |
212 | /// See also: |
213 | /// |
214 | /// * [TextField], which is a Material Design text field that can be controlled |
215 | /// with a [TextEditingController]. |
216 | /// * [EditableText], which is a raw region of editable text that can be |
217 | /// controlled with a [TextEditingController]. |
218 | /// * Learn how to use a [TextEditingController] in one of our [cookbook recipes](https://docs.flutter.dev/cookbook/forms/text-field-changes#2-use-a-texteditingcontroller). |
219 | class TextEditingController extends ValueNotifier<TextEditingValue> { |
220 | /// Creates a controller for an editable text field, with no initial selection. |
221 | /// |
222 | /// This constructor treats a null [text] argument as if it were the empty |
223 | /// string. |
224 | /// |
225 | /// The initial selection is `TextSelection.collapsed(offset: -1)`. |
226 | /// This indicates that there is no selection at all ([TextSelection.isValid] |
227 | /// is false in this case). When a text field is built with a controller whose |
228 | /// selection is not valid, the text field will update the selection when it |
229 | /// is focused (the selection will be an empty selection positioned at the |
230 | /// end of the text). |
231 | /// |
232 | /// Consider using [TextEditingController.fromValue] to initialize both the |
233 | /// text and the selection. |
234 | /// |
235 | /// {@tool dartpad} |
236 | /// This example creates a [TextField] with a [TextEditingController] whose |
237 | /// initial selection is empty (collapsed) and positioned at the beginning |
238 | /// of the text (offset is 0). |
239 | /// |
240 | /// ** See code in examples/api/lib/widgets/editable_text/text_editing_controller.1.dart ** |
241 | /// {@end-tool} |
242 | TextEditingController({String? text}) |
243 | : super(text == null ? TextEditingValue.empty : TextEditingValue(text: text)); |
244 | |
245 | /// Creates a controller for an editable text field from an initial [TextEditingValue]. |
246 | /// |
247 | /// This constructor treats a null [value] argument as if it were |
248 | /// [TextEditingValue.empty]. |
249 | TextEditingController.fromValue(TextEditingValue? value) |
250 | : assert( |
251 | value == null || !value.composing.isValid || value.isComposingRangeValid, |
252 | 'New TextEditingValue$value has an invalid non-empty composing range ' |
253 | '${value.composing} . It is recommended to use a valid composing range, ' |
254 | 'even for readonly text fields.', |
255 | ), |
256 | super(value ?? TextEditingValue.empty); |
257 | |
258 | /// The current string the user is editing. |
259 | String get text => value.text; |
260 | |
261 | /// Updates the current [text] to the given `newText`, and removes existing |
262 | /// selection and composing range held by the controller. |
263 | /// |
264 | /// This setter is typically only used in tests, as it resets the cursor |
265 | /// position and the composing state. For production code, **consider using the |
266 | /// [value] setter to update the [text] value instead**, and specify a |
267 | /// reasonable selection range within the new [text]. |
268 | /// |
269 | /// Setting this notifies all the listeners of this [TextEditingController] |
270 | /// that they need to update (it calls [notifyListeners]). For this reason, |
271 | /// this value should only be set between frames, e.g. in response to user |
272 | /// actions, not during the build, layout, or paint phases. This property can |
273 | /// be set from a listener added to this [TextEditingController]. |
274 | set text(String newText) { |
275 | value = value.copyWith( |
276 | text: newText, |
277 | selection: const TextSelection.collapsed(offset: -1), |
278 | composing: TextRange.empty, |
279 | ); |
280 | } |
281 | |
282 | @override |
283 | set value(TextEditingValue newValue) { |
284 | assert( |
285 | !newValue.composing.isValid || newValue.isComposingRangeValid, |
286 | 'New TextEditingValue$newValue has an invalid non-empty composing range ' |
287 | '${newValue.composing} . It is recommended to use a valid composing range, ' |
288 | 'even for readonly text fields.', |
289 | ); |
290 | super.value = newValue; |
291 | } |
292 | |
293 | /// Builds [TextSpan] from current editing value. |
294 | /// |
295 | /// By default makes text in composing range appear as underlined. Descendants |
296 | /// can override this method to customize appearance of text. |
297 | TextSpan buildTextSpan({ |
298 | required BuildContext context, |
299 | TextStyle? style, |
300 | required bool withComposing, |
301 | }) { |
302 | assert(!value.composing.isValid || !withComposing || value.isComposingRangeValid); |
303 | // If the composing range is out of range for the current text, ignore it to |
304 | // preserve the tree integrity, otherwise in release mode a RangeError will |
305 | // be thrown and this EditableText will be built with a broken subtree. |
306 | final bool composingRegionOutOfRange = !value.isComposingRangeValid || !withComposing; |
307 | |
308 | if (composingRegionOutOfRange) { |
309 | return TextSpan(style: style, text: text); |
310 | } |
311 | |
312 | final TextStyle composingStyle = |
313 | style?.merge(const TextStyle(decoration: TextDecoration.underline)) ?? |
314 | const TextStyle(decoration: TextDecoration.underline); |
315 | return TextSpan( |
316 | style: style, |
317 | children: <TextSpan>[ |
318 | TextSpan(text: value.composing.textBefore(value.text)), |
319 | TextSpan(style: composingStyle, text: value.composing.textInside(value.text)), |
320 | TextSpan(text: value.composing.textAfter(value.text)), |
321 | ], |
322 | ); |
323 | } |
324 | |
325 | /// The currently selected range within [text]. |
326 | /// |
327 | /// If the selection is collapsed, then this property gives the offset of the |
328 | /// cursor within the text. |
329 | TextSelection get selection => value.selection; |
330 | |
331 | /// Setting this will notify all the listeners of this [TextEditingController] |
332 | /// that they need to update (it calls [notifyListeners]). For this reason, |
333 | /// this value should only be set between frames, e.g. in response to user |
334 | /// actions, not during the build, layout, or paint phases. |
335 | /// |
336 | /// This property can be set from a listener added to this |
337 | /// [TextEditingController]; however, one should not also set [text] |
338 | /// in a separate statement. To change both the [text] and the [selection] |
339 | /// change the controller's [value]. |
340 | /// |
341 | /// If the new selection is outside the composing range, the composing range is |
342 | /// cleared. |
343 | set selection(TextSelection newSelection) { |
344 | if (text.length < newSelection.end || text.length < newSelection.start) { |
345 | throw FlutterError('invalid text selection:$newSelection '); |
346 | } |
347 | final TextRange newComposing = |
348 | _isSelectionWithinComposingRange(newSelection) ? value.composing : TextRange.empty; |
349 | value = value.copyWith(selection: newSelection, composing: newComposing); |
350 | } |
351 | |
352 | /// Set the [value] to empty. |
353 | /// |
354 | /// After calling this function, [text] will be the empty string and the |
355 | /// selection will be collapsed at zero offset. |
356 | /// |
357 | /// Calling this will notify all the listeners of this [TextEditingController] |
358 | /// that they need to update (it calls [notifyListeners]). For this reason, |
359 | /// this method should only be called between frames, e.g. in response to user |
360 | /// actions, not during the build, layout, or paint phases. |
361 | void clear() { |
362 | value = const TextEditingValue(selection: TextSelection.collapsed(offset: 0)); |
363 | } |
364 | |
365 | /// Set the composing region to an empty range. |
366 | /// |
367 | /// The composing region is the range of text that is still being composed. |
368 | /// Calling this function indicates that the user is done composing that |
369 | /// region. |
370 | /// |
371 | /// Calling this will notify all the listeners of this [TextEditingController] |
372 | /// that they need to update (it calls [notifyListeners]). For this reason, |
373 | /// this method should only be called between frames, e.g. in response to user |
374 | /// actions, not during the build, layout, or paint phases. |
375 | void clearComposing() { |
376 | value = value.copyWith(composing: TextRange.empty); |
377 | } |
378 | |
379 | /// Check that the [selection] is inside of the composing range. |
380 | bool _isSelectionWithinComposingRange(TextSelection selection) { |
381 | return selection.start >= value.composing.start && selection.end <= value.composing.end; |
382 | } |
383 | } |
384 | |
385 | /// Toolbar configuration for [EditableText]. |
386 | /// |
387 | /// Toolbar is a context menu that will show up when user right click or long |
388 | /// press the [EditableText]. It includes several options: cut, copy, paste, |
389 | /// and select all. |
390 | /// |
391 | /// [EditableText] and its derived widgets have their own default [ToolbarOptions]. |
392 | /// Create a custom [ToolbarOptions] if you want explicit control over the toolbar |
393 | /// option. |
394 | @Deprecated( |
395 | 'Use `contextMenuBuilder` instead. ' |
396 | 'This feature was deprecated after v3.3.0-0.5.pre.', |
397 | ) |
398 | class ToolbarOptions { |
399 | /// Create a toolbar configuration with given options. |
400 | /// |
401 | /// All options default to false if they are not explicitly set. |
402 | @Deprecated( |
403 | 'Use `contextMenuBuilder` instead. ' |
404 | 'This feature was deprecated after v3.3.0-0.5.pre.', |
405 | ) |
406 | const ToolbarOptions({ |
407 | this.copy = false, |
408 | this.cut = false, |
409 | this.paste = false, |
410 | this.selectAll = false, |
411 | }); |
412 | |
413 | /// An instance of [ToolbarOptions] with no options enabled. |
414 | static const ToolbarOptions empty = ToolbarOptions(); |
415 | |
416 | /// Whether to show copy option in toolbar. |
417 | /// |
418 | /// Defaults to false. |
419 | final bool copy; |
420 | |
421 | /// Whether to show cut option in toolbar. |
422 | /// |
423 | /// If [EditableText.readOnly] is set to true, cut will be disabled regardless. |
424 | /// |
425 | /// Defaults to false. |
426 | final bool cut; |
427 | |
428 | /// Whether to show paste option in toolbar. |
429 | /// |
430 | /// If [EditableText.readOnly] is set to true, paste will be disabled regardless. |
431 | /// |
432 | /// Defaults to false. |
433 | final bool paste; |
434 | |
435 | /// Whether to show select all option in toolbar. |
436 | /// |
437 | /// Defaults to false. |
438 | final bool selectAll; |
439 | } |
440 | |
441 | /// Configures the ability to insert media content through the soft keyboard. |
442 | /// |
443 | /// The configuration provides a handler for any rich content inserted through |
444 | /// the system input method, and also provides the ability to limit the mime |
445 | /// types of the inserted content. |
446 | /// |
447 | /// See also: |
448 | /// |
449 | /// * [EditableText.contentInsertionConfiguration] |
450 | class ContentInsertionConfiguration { |
451 | /// Creates a content insertion configuration with the specified options. |
452 | /// |
453 | /// A handler for inserted content, in the form of [onContentInserted], must |
454 | /// be supplied. |
455 | /// |
456 | /// The allowable mime types of inserted content may also |
457 | /// be provided via [allowedMimeTypes], which cannot be an empty list. |
458 | ContentInsertionConfiguration({ |
459 | required this.onContentInserted, |
460 | this.allowedMimeTypes = kDefaultContentInsertionMimeTypes, |
461 | }) : assert(allowedMimeTypes.isNotEmpty); |
462 | |
463 | /// Called when a user inserts content through the virtual / on-screen keyboard, |
464 | /// currently only used on Android. |
465 | /// |
466 | /// [KeyboardInsertedContent] holds the data representing the inserted content. |
467 | /// |
468 | /// {@tool dartpad} |
469 | /// |
470 | /// This example shows how to access the data for inserted content in your |
471 | /// `TextField`. |
472 | /// |
473 | /// ** See code in examples/api/lib/widgets/editable_text/editable_text.on_content_inserted.0.dart ** |
474 | /// {@end-tool} |
475 | /// |
476 | /// See also: |
477 | /// |
478 | /// * <https://developer.android.com/guide/topics/text/image-keyboard> |
479 | final ValueChanged<KeyboardInsertedContent> onContentInserted; |
480 | |
481 | /// {@template flutter.widgets.contentInsertionConfiguration.allowedMimeTypes} |
482 | /// Used when a user inserts image-based content through the device keyboard, |
483 | /// currently only used on Android. |
484 | /// |
485 | /// The passed list of strings will determine which MIME types are allowed to |
486 | /// be inserted via the device keyboard. |
487 | /// |
488 | /// The default mime types are given by [kDefaultContentInsertionMimeTypes]. |
489 | /// These are all the mime types that are able to be handled and inserted |
490 | /// from keyboards. |
491 | /// |
492 | /// This field cannot be an empty list. |
493 | /// |
494 | /// {@tool dartpad} |
495 | /// This example shows how to limit image insertion to specific file types. |
496 | /// |
497 | /// ** See code in examples/api/lib/widgets/editable_text/editable_text.on_content_inserted.0.dart ** |
498 | /// {@end-tool} |
499 | /// |
500 | /// See also: |
501 | /// |
502 | /// * <https://developer.android.com/guide/topics/text/image-keyboard> |
503 | /// {@endtemplate} |
504 | final List<String> allowedMimeTypes; |
505 | } |
506 | |
507 | // A time-value pair that represents a key frame in an animation. |
508 | class _KeyFrame { |
509 | const _KeyFrame(this.time, this.value); |
510 | // Values extracted from iOS 15.4 UIKit. |
511 | static const List<_KeyFrame> iOSBlinkingCaretKeyFrames = <_KeyFrame>[ |
512 | _KeyFrame(0, 1), // 0 |
513 | _KeyFrame(0.5, 1), // 1 |
514 | _KeyFrame(0.5375, 0.75), // 2 |
515 | _KeyFrame(0.575, 0.5), // 3 |
516 | _KeyFrame(0.6125, 0.25), // 4 |
517 | _KeyFrame(0.65, 0), // 5 |
518 | _KeyFrame(0.85, 0), // 6 |
519 | _KeyFrame(0.8875, 0.25), // 7 |
520 | _KeyFrame(0.925, 0.5), // 8 |
521 | _KeyFrame(0.9625, 0.75), // 9 |
522 | _KeyFrame(1, 1), // 10 |
523 | ]; |
524 | |
525 | // The timing, in seconds, of the specified animation `value`. |
526 | final double time; |
527 | final double value; |
528 | } |
529 | |
530 | class _DiscreteKeyFrameSimulation extends Simulation { |
531 | _DiscreteKeyFrameSimulation.iOSBlinkingCaret() : this._(_KeyFrame.iOSBlinkingCaretKeyFrames, 1); |
532 | _DiscreteKeyFrameSimulation._(this._keyFrames, this.maxDuration) |
533 | : assert(_keyFrames.isNotEmpty), |
534 | assert(_keyFrames.last.time <= maxDuration), |
535 | assert(() { |
536 | for (int i = 0; i < _keyFrames.length - 1; i += 1) { |
537 | if (_keyFrames[i].time > _keyFrames[i + 1].time) { |
538 | return false; |
539 | } |
540 | } |
541 | return true; |
542 | }(), 'The key frame sequence must be sorted by time.'); |
543 | |
544 | final double maxDuration; |
545 | |
546 | final List<_KeyFrame> _keyFrames; |
547 | |
548 | @override |
549 | double dx(double time) => 0; |
550 | |
551 | @override |
552 | bool isDone(double time) => time >= maxDuration; |
553 | |
554 | // The index of the KeyFrame corresponds to the most recent input `time`. |
555 | int _lastKeyFrameIndex = 0; |
556 | |
557 | @override |
558 | double x(double time) { |
559 | final int length = _keyFrames.length; |
560 | |
561 | // Perform a linear search in the sorted key frame list, starting from the |
562 | // last key frame found, since the input `time` usually monotonically |
563 | // increases by a small amount. |
564 | int searchIndex; |
565 | final int endIndex; |
566 | if (_keyFrames[_lastKeyFrameIndex].time > time) { |
567 | // The simulation may have restarted. Search within the index range |
568 | // [0, _lastKeyFrameIndex). |
569 | searchIndex = 0; |
570 | endIndex = _lastKeyFrameIndex; |
571 | } else { |
572 | searchIndex = _lastKeyFrameIndex; |
573 | endIndex = length; |
574 | } |
575 | |
576 | // Find the target key frame. Don't have to check (endIndex - 1): if |
577 | // (endIndex - 2) doesn't work we'll have to pick (endIndex - 1) anyways. |
578 | while (searchIndex < endIndex - 1) { |
579 | assert(_keyFrames[searchIndex].time <= time); |
580 | final _KeyFrame next = _keyFrames[searchIndex + 1]; |
581 | if (time < next.time) { |
582 | break; |
583 | } |
584 | searchIndex += 1; |
585 | } |
586 | |
587 | _lastKeyFrameIndex = searchIndex; |
588 | return _keyFrames[_lastKeyFrameIndex].value; |
589 | } |
590 | } |
591 | |
592 | /// A basic text input field. |
593 | /// |
594 | /// This widget interacts with the [TextInput] service to let the user edit the |
595 | /// text it contains. It also provides scrolling, selection, and cursor |
596 | /// movement. |
597 | /// |
598 | /// The [EditableText] widget is a low-level widget that is intended as a |
599 | /// building block for custom widget sets. For a complete user experience, |
600 | /// consider using a [TextField] or [CupertinoTextField]. |
601 | /// |
602 | /// ## Handling User Input |
603 | /// |
604 | /// Currently the user may change the text this widget contains via keyboard or |
605 | /// the text selection menu. When the user inserted or deleted text, you will be |
606 | /// notified of the change and get a chance to modify the new text value: |
607 | /// |
608 | /// * The [inputFormatters] will be first applied to the user input. |
609 | /// |
610 | /// * The [controller]'s [TextEditingController.value] will be updated with the |
611 | /// formatted result, and the [controller]'s listeners will be notified. |
612 | /// |
613 | /// * The [onChanged] callback, if specified, will be called last. |
614 | /// |
615 | /// ## Input Actions |
616 | /// |
617 | /// A [TextInputAction] can be provided to customize the appearance of the |
618 | /// action button on the soft keyboard for Android and iOS. The default action |
619 | /// is [TextInputAction.done]. |
620 | /// |
621 | /// Many [TextInputAction]s are common between Android and iOS. However, if a |
622 | /// [textInputAction] is provided that is not supported by the current |
623 | /// platform in debug mode, an error will be thrown when the corresponding |
624 | /// EditableText receives focus. For example, providing iOS's "emergencyCall" |
625 | /// action when running on an Android device will result in an error when in |
626 | /// debug mode. In release mode, incompatible [TextInputAction]s are replaced |
627 | /// either with "unspecified" on Android, or "default" on iOS. Appropriate |
628 | /// [textInputAction]s can be chosen by checking the current platform and then |
629 | /// selecting the appropriate action. |
630 | /// |
631 | /// {@template flutter.widgets.EditableText.lifeCycle} |
632 | /// ## Lifecycle |
633 | /// |
634 | /// Upon completion of editing, like pressing the "done" button on the keyboard, |
635 | /// two actions take place: |
636 | /// |
637 | /// 1st: Editing is finalized. The default behavior of this step includes |
638 | /// an invocation of [onChanged]. That default behavior can be overridden. |
639 | /// See [onEditingComplete] for details. |
640 | /// |
641 | /// 2nd: [onSubmitted] is invoked with the user's input value. |
642 | /// |
643 | /// [onSubmitted] can be used to manually move focus to another input widget |
644 | /// when a user finishes with the currently focused input widget. |
645 | /// |
646 | /// When the widget has focus, it will prevent itself from disposing via |
647 | /// [AutomaticKeepAliveClientMixin.wantKeepAlive] in order to avoid losing the |
648 | /// selection. Removing the focus will allow it to be disposed. |
649 | /// {@endtemplate} |
650 | /// |
651 | /// Rather than using this widget directly, consider using [TextField], which |
652 | /// is a full-featured, material-design text input field with placeholder text, |
653 | /// labels, and [Form] integration. |
654 | /// |
655 | /// ## Text Editing [Intent]s and Their Default [Action]s |
656 | /// |
657 | /// This widget provides default [Action]s for handling common text editing |
658 | /// [Intent]s such as deleting, copying and pasting in the text field. These |
659 | /// [Action]s can be directly invoked using [Actions.invoke] or the |
660 | /// [Actions.maybeInvoke] method. The default text editing keyboard [Shortcuts], |
661 | /// typically declared in [DefaultTextEditingShortcuts], also use these |
662 | /// [Intent]s and [Action]s to perform the text editing operations they are |
663 | /// bound to. |
664 | /// |
665 | /// The default handling of a specific [Intent] can be overridden by placing an |
666 | /// [Actions] widget above this widget. See the [Action] class and the |
667 | /// [Action.overridable] constructor for more information on how a pre-defined |
668 | /// overridable [Action] can be overridden. |
669 | /// |
670 | /// ### Intents for Deleting Text and Their Default Behavior |
671 | /// |
672 | /// | **Intent Class** | **Default Behavior when there's selected text** | **Default Behavior when there is a [caret](https://en.wikipedia.org/wiki/Caret_navigation) (The selection is [TextSelection.collapsed])** | |
673 | /// | :------------------------------- | :--------------------------------------------------- | :----------------------------------------------------------------------- | |
674 | /// | [DeleteCharacterIntent] | Deletes the selected text | Deletes the user-perceived character before or after the caret location. | |
675 | /// | [DeleteToNextWordBoundaryIntent] | Deletes the selected text and the word before/after the selection's [TextSelection.extent] position | Deletes from the caret location to the previous or the next word boundary | |
676 | /// | [DeleteToLineBreakIntent] | Deletes the selected text, and deletes to the start/end of the line from the selection's [TextSelection.extent] position | Deletes from the caret location to the logical start or end of the current line | |
677 | /// |
678 | /// ### Intents for Moving the [Caret](https://en.wikipedia.org/wiki/Caret_navigation) |
679 | /// |
680 | /// | **Intent Class** | **Default Behavior when there's selected text** | **Default Behavior when there is a caret ([TextSelection.collapsed])** | |
681 | /// | :----------------------------------------------------------------------------------- | :--------------------------------------------------------------- | :---------------------------------------------------------------------- | |
682 | /// | [ExtendSelectionByCharacterIntent](`collapseSelection: true`) | Collapses the selection to the logical start/end of the selection | Moves the caret past the user-perceived character before or after the current caret location. | |
683 | /// | [ExtendSelectionToNextWordBoundaryIntent](`collapseSelection: true`) | Collapses the selection to the word boundary before/after the selection's [TextSelection.extent] position | Moves the caret to the previous/next word boundary. | |
684 | /// | [ExtendSelectionToNextWordBoundaryOrCaretLocationIntent](`collapseSelection: true`) | Collapses the selection to the word boundary before/after the selection's [TextSelection.extent] position, or [TextSelection.base], whichever is closest in the given direction | Moves the caret to the previous/next word boundary. | |
685 | /// | [ExtendSelectionToLineBreakIntent](`collapseSelection: true`) | Collapses the selection to the start/end of the line at the selection's [TextSelection.extent] position | Moves the caret to the start/end of the current line .| |
686 | /// | [ExtendSelectionVerticallyToAdjacentLineIntent](`collapseSelection: true`) | Collapses the selection to the position closest to the selection's [TextSelection.extent], on the previous/next adjacent line | Moves the caret to the closest position on the previous/next adjacent line. | |
687 | /// | [ExtendSelectionVerticallyToAdjacentPageIntent](`collapseSelection: true`) | Collapses the selection to the position closest to the selection's [TextSelection.extent], on the previous/next adjacent page | Moves the caret to the closest position on the previous/next adjacent page. | |
688 | /// | [ExtendSelectionToDocumentBoundaryIntent](`collapseSelection: true`) | Collapses the selection to the start/end of the document | Moves the caret to the start/end of the document. | |
689 | /// |
690 | /// #### Intents for Extending the Selection |
691 | /// |
692 | /// | **Intent Class** | **Default Behavior when there's selected text** | **Default Behavior when there is a caret ([TextSelection.collapsed])** | |
693 | /// | :----------------------------------------------------------------------------------- | :--------------------------------------------------------------- | :---------------------------------------------------------------------- | |
694 | /// | [ExtendSelectionByCharacterIntent](`collapseSelection: false`) | Moves the selection's [TextSelection.extent] past the user-perceived character before/after it | |
695 | /// | [ExtendSelectionToNextWordBoundaryIntent](`collapseSelection: false`) | Moves the selection's [TextSelection.extent] to the previous/next word boundary | |
696 | /// | [ExtendSelectionToNextWordBoundaryOrCaretLocationIntent](`collapseSelection: false`) | Moves the selection's [TextSelection.extent] to the previous/next word boundary, or [TextSelection.base] whichever is closest in the given direction | Moves the selection's [TextSelection.extent] to the previous/next word boundary. | |
697 | /// | [ExtendSelectionToLineBreakIntent](`collapseSelection: false`) | Moves the selection's [TextSelection.extent] to the start/end of the line | |
698 | /// | [ExtendSelectionVerticallyToAdjacentLineIntent](`collapseSelection: false`) | Moves the selection's [TextSelection.extent] to the closest position on the previous/next adjacent line | |
699 | /// | [ExtendSelectionVerticallyToAdjacentPageIntent](`collapseSelection: false`) | Moves the selection's [TextSelection.extent] to the closest position on the previous/next adjacent page | |
700 | /// | [ExtendSelectionToDocumentBoundaryIntent](`collapseSelection: false`) | Moves the selection's [TextSelection.extent] to the start/end of the document | |
701 | /// | [SelectAllTextIntent] | Selects the entire document | |
702 | /// |
703 | /// ### Other Intents |
704 | /// |
705 | /// | **Intent Class** | **Default Behavior** | |
706 | /// | :-------------------------------------- | :--------------------------------------------------- | |
707 | /// | [DoNothingAndStopPropagationTextIntent] | Does nothing in the input field, and prevents the key event from further propagating in the widget tree. | |
708 | /// | [ReplaceTextIntent] | Replaces the current [TextEditingValue] in the input field's [TextEditingController], and triggers all related user callbacks and [TextInputFormatter]s. | |
709 | /// | [UpdateSelectionIntent] | Updates the current selection in the input field's [TextEditingController], and triggers the [onSelectionChanged] callback. | |
710 | /// | [CopySelectionTextIntent] | Copies or cuts the selected text into the clipboard | |
711 | /// | [PasteTextIntent] | Inserts the current text in the clipboard after the caret location, or replaces the selected text if the selection is not collapsed. | |
712 | /// |
713 | /// ## Text Editing [Shortcuts] |
714 | /// |
715 | /// It's also possible to directly remap keyboard shortcuts to new [Intent]s by |
716 | /// inserting a [Shortcuts] widget above this in the widget tree. When using |
717 | /// [WidgetsApp], the large set of default text editing keyboard shortcuts are |
718 | /// declared near the top of the widget tree in [DefaultTextEditingShortcuts], |
719 | /// and any [Shortcuts] widget between it and this [EditableText] will override |
720 | /// those defaults. |
721 | /// |
722 | /// {@template flutter.widgets.editableText.shortcutsAndTextInput} |
723 | /// ### Interactions Between [Shortcuts] and Text Input |
724 | /// |
725 | /// Shortcuts prevent text input fields from receiving their keystrokes as text |
726 | /// input. For example, placing a [Shortcuts] widget in the widget tree above |
727 | /// a text input field and creating a shortcut for [LogicalKeyboardKey.keyA] |
728 | /// will prevent the field from receiving that key as text input. In other |
729 | /// words, typing key "A" into the field will trigger the shortcut and will not |
730 | /// insert a letter "a" into the field. |
731 | /// |
732 | /// This happens because of the way that key strokes are handled in Flutter. |
733 | /// When a keystroke is received in Flutter's engine, it first gives the |
734 | /// framework the opportunity to handle it as a raw key event through |
735 | /// [SystemChannels.keyEvent]. This is what [Shortcuts] listens to indirectly |
736 | /// through its [FocusNode]. If it is not handled, then it will proceed to try |
737 | /// handling it as text input through [SystemChannels.textInput], which is what |
738 | /// [EditableTextState] listens to through [TextInputClient]. |
739 | /// |
740 | /// This behavior, where a shortcut prevents text input into some field, can be |
741 | /// overridden by using another [Shortcuts] widget lower in the widget tree and |
742 | /// mapping the desired key stroke(s) to [DoNothingAndStopPropagationIntent]. |
743 | /// The key event will be reported as unhandled by the framework and will then |
744 | /// be sent as text input as usual. |
745 | /// {@endtemplate} |
746 | /// |
747 | /// ## Gesture Events Handling |
748 | /// |
749 | /// When [rendererIgnoresPointer] is false (the default), this widget provides |
750 | /// rudimentary, platform-agnostic gesture handling for user actions such as |
751 | /// tapping, long-pressing, and scrolling. |
752 | /// |
753 | /// To provide more complete gesture handling, including double-click to select |
754 | /// a word, drag selection, and platform-specific handling of gestures such as |
755 | /// long presses, consider setting [rendererIgnoresPointer] to true and using |
756 | /// [TextSelectionGestureDetectorBuilder]. |
757 | /// |
758 | /// {@template flutter.widgets.editableText.showCaretOnScreen} |
759 | /// ## Keep the caret visible when focused |
760 | /// |
761 | /// When focused, this widget will make attempts to keep the text area and its |
762 | /// caret (even when [showCursor] is `false`) visible, on these occasions: |
763 | /// |
764 | /// * When the user focuses this text field and it is not [readOnly]. |
765 | /// * When the user changes the selection of the text field, or changes the |
766 | /// text when the text field is not [readOnly]. |
767 | /// * When the virtual keyboard pops up. |
768 | /// {@endtemplate} |
769 | /// |
770 | /// ## Scrolling Considerations |
771 | /// |
772 | /// If this [EditableText] is not a descendant of [Scaffold] and is being used |
773 | /// within a [Scrollable] or nested [Scrollable]s, consider placing a |
774 | /// [ScrollNotificationObserver] above the root [Scrollable] that contains this |
775 | /// [EditableText] to ensure proper scroll coordination for [EditableText] and |
776 | /// its components like [TextSelectionOverlay]. |
777 | /// |
778 | /// {@template flutter.widgets.editableText.accessibility} |
779 | /// ## Troubleshooting Common Accessibility Issues |
780 | /// |
781 | /// ### Customizing User Input Accessibility Announcements |
782 | /// |
783 | /// To customize user input accessibility announcements triggered by text |
784 | /// changes, use [SemanticsService.announce] to make the desired |
785 | /// accessibility announcement. |
786 | /// |
787 | /// On iOS, the on-screen keyboard may announce the most recent input |
788 | /// incorrectly when a [TextInputFormatter] inserts a thousands separator to |
789 | /// a currency value text field. The following example demonstrates how to |
790 | /// suppress the default accessibility announcements by always announcing |
791 | /// the content of the text field as a US currency value (the `\$` inserts |
792 | /// a dollar sign, the `$newText` interpolates the `newText` variable): |
793 | /// |
794 | /// ```dart |
795 | /// onChanged: (String newText) { |
796 | /// if (newText.isNotEmpty) { |
797 | /// SemanticsService.announce('\$$newText', Directionality.of(context)); |
798 | /// } |
799 | /// } |
800 | /// ``` |
801 | /// |
802 | /// {@endtemplate} |
803 | /// |
804 | /// See also: |
805 | /// |
806 | /// * [TextField], which is a full-featured, material-design text input field |
807 | /// with placeholder text, labels, and [Form] integration. |
808 | class EditableText extends StatefulWidget { |
809 | /// Creates a basic text input control. |
810 | /// |
811 | /// The [maxLines] property can be set to null to remove the restriction on |
812 | /// the number of lines. By default, it is one, meaning this is a single-line |
813 | /// text field. [maxLines] must be null or greater than zero. |
814 | /// |
815 | /// If [keyboardType] is not set or is null, its value will be inferred from |
816 | /// [autofillHints], if [autofillHints] is not empty. Otherwise it defaults to |
817 | /// [TextInputType.text] if [maxLines] is exactly one, and |
818 | /// [TextInputType.multiline] if [maxLines] is null or greater than one. |
819 | /// |
820 | /// The text cursor is not shown if [showCursor] is false or if [showCursor] |
821 | /// is null (the default) and [readOnly] is true. |
822 | EditableText({ |
823 | super.key, |
824 | required this.controller, |
825 | required this.focusNode, |
826 | this.readOnly = false, |
827 | this.obscuringCharacter = '•', |
828 | this.obscureText = false, |
829 | this.autocorrect = true, |
830 | SmartDashesType? smartDashesType, |
831 | SmartQuotesType? smartQuotesType, |
832 | this.enableSuggestions = true, |
833 | required this.style, |
834 | StrutStyle? strutStyle, |
835 | required this.cursorColor, |
836 | required this.backgroundCursorColor, |
837 | this.textAlign = TextAlign.start, |
838 | this.textDirection, |
839 | this.locale, |
840 | @Deprecated( |
841 | 'Use textScaler instead. ' |
842 | 'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. ' |
843 | 'This feature was deprecated after v3.12.0-2.0.pre.', |
844 | ) |
845 | this.textScaleFactor, |
846 | this.textScaler, |
847 | this.maxLines = 1, |
848 | this.minLines, |
849 | this.expands = false, |
850 | this.forceLine = true, |
851 | this.textHeightBehavior, |
852 | this.textWidthBasis = TextWidthBasis.parent, |
853 | this.autofocus = false, |
854 | bool? showCursor, |
855 | this.showSelectionHandles = false, |
856 | this.selectionColor, |
857 | this.selectionControls, |
858 | TextInputType? keyboardType, |
859 | this.textInputAction, |
860 | this.textCapitalization = TextCapitalization.none, |
861 | this.onChanged, |
862 | this.onEditingComplete, |
863 | this.onSubmitted, |
864 | this.onAppPrivateCommand, |
865 | this.onSelectionChanged, |
866 | this.onSelectionHandleTapped, |
867 | this.groupId = EditableText, |
868 | this.onTapOutside, |
869 | this.onTapUpOutside, |
870 | List<TextInputFormatter>? inputFormatters, |
871 | this.mouseCursor, |
872 | this.rendererIgnoresPointer = false, |
873 | this.cursorWidth = 2.0, |
874 | this.cursorHeight, |
875 | this.cursorRadius, |
876 | this.cursorOpacityAnimates = false, |
877 | this.cursorOffset, |
878 | this.paintCursorAboveText = false, |
879 | this.selectionHeightStyle = ui.BoxHeightStyle.tight, |
880 | this.selectionWidthStyle = ui.BoxWidthStyle.tight, |
881 | this.scrollPadding = const EdgeInsets.all(20.0), |
882 | this.keyboardAppearance = Brightness.light, |
883 | this.dragStartBehavior = DragStartBehavior.start, |
884 | bool? enableInteractiveSelection, |
885 | this.scrollController, |
886 | this.scrollPhysics, |
887 | this.autocorrectionTextRectColor, |
888 | @Deprecated( |
889 | 'Use `contextMenuBuilder` instead. ' |
890 | 'This feature was deprecated after v3.3.0-0.5.pre.', |
891 | ) |
892 | ToolbarOptions? toolbarOptions, |
893 | this.autofillHints = const <String>[], |
894 | this.autofillClient, |
895 | this.clipBehavior = Clip.hardEdge, |
896 | this.restorationId, |
897 | this.scrollBehavior, |
898 | @Deprecated( |
899 | 'Use `stylusHandwritingEnabled` instead. ' |
900 | 'This feature was deprecated after v3.27.0-0.2.pre.', |
901 | ) |
902 | this.scribbleEnabled = true, |
903 | this.stylusHandwritingEnabled = defaultStylusHandwritingEnabled, |
904 | this.enableIMEPersonalizedLearning = true, |
905 | this.contentInsertionConfiguration, |
906 | this.contextMenuBuilder, |
907 | this.spellCheckConfiguration, |
908 | this.magnifierConfiguration = TextMagnifierConfiguration.disabled, |
909 | this.undoController, |
910 | }) : assert(obscuringCharacter.length == 1), |
911 | smartDashesType = |
912 | smartDashesType ?? (obscureText ? SmartDashesType.disabled : SmartDashesType.enabled), |
913 | smartQuotesType = |
914 | smartQuotesType ?? (obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled), |
915 | assert(minLines == null || minLines > 0), |
916 | assert( |
917 | (maxLines == null) || (minLines == null) || (maxLines >= minLines), |
918 | "minLines can't be greater than maxLines", |
919 | ), |
920 | assert( |
921 | !expands || (maxLines == null && minLines == null), |
922 | 'minLines and maxLines must be null when expands is true.', |
923 | ), |
924 | assert(!obscureText || maxLines == 1, 'Obscured fields cannot be multiline.'), |
925 | enableInteractiveSelection = enableInteractiveSelection ?? (!readOnly || !obscureText), |
926 | toolbarOptions = |
927 | selectionControls is TextSelectionHandleControls && toolbarOptions == null |
928 | ? ToolbarOptions.empty |
929 | : toolbarOptions ?? |
930 | (obscureText |
931 | ? (readOnly |
932 | // No point in even offering "Select All" in a read-only obscured |
933 | // field. |
934 | ? ToolbarOptions.empty |
935 | // Writable, but obscured. |
936 | : const ToolbarOptions(selectAll: true, paste: true)) |
937 | : (readOnly |
938 | // Read-only, not obscured. |
939 | ? const ToolbarOptions(selectAll: true, copy: true) |
940 | // Writable, not obscured. |
941 | : const ToolbarOptions( |
942 | copy: true, |
943 | cut: true, |
944 | selectAll: true, |
945 | paste: true, |
946 | ))), |
947 | assert( |
948 | spellCheckConfiguration == null || |
949 | spellCheckConfiguration == const SpellCheckConfiguration.disabled() || |
950 | spellCheckConfiguration.misspelledTextStyle != null, |
951 | 'spellCheckConfiguration must specify a misspelledTextStyle if spell check behavior is desired', |
952 | ), |
953 | _strutStyle = strutStyle, |
954 | keyboardType = |
955 | keyboardType ?? _inferKeyboardType(autofillHints: autofillHints, maxLines: maxLines), |
956 | inputFormatters = |
957 | maxLines == 1 |
958 | ? <TextInputFormatter>[ |
959 | FilteringTextInputFormatter.singleLineFormatter, |
960 | ...inputFormatters ?? const Iterable<TextInputFormatter>.empty(), |
961 | ] |
962 | : inputFormatters, |
963 | showCursor = showCursor ?? !readOnly; |
964 | |
965 | /// Controls the text being edited. |
966 | final TextEditingController controller; |
967 | |
968 | /// Controls whether this widget has keyboard focus. |
969 | final FocusNode focusNode; |
970 | |
971 | /// {@template flutter.widgets.editableText.obscuringCharacter} |
972 | /// Character used for obscuring text if [obscureText] is true. |
973 | /// |
974 | /// Must be only a single character. |
975 | /// |
976 | /// Defaults to the character U+2022 BULLET (•). |
977 | /// {@endtemplate} |
978 | final String obscuringCharacter; |
979 | |
980 | /// {@template flutter.widgets.editableText.obscureText} |
981 | /// Whether to hide the text being edited (e.g., for passwords). |
982 | /// |
983 | /// When this is set to true, all the characters in the text field are |
984 | /// replaced by [obscuringCharacter], and the text in the field cannot be |
985 | /// copied with copy or cut. If [readOnly] is also true, then the text cannot |
986 | /// be selected. |
987 | /// |
988 | /// Defaults to false. |
989 | /// {@endtemplate} |
990 | final bool obscureText; |
991 | |
992 | /// {@macro dart.ui.textHeightBehavior} |
993 | final TextHeightBehavior? textHeightBehavior; |
994 | |
995 | /// {@macro flutter.painting.textPainter.textWidthBasis} |
996 | final TextWidthBasis textWidthBasis; |
997 | |
998 | /// {@template flutter.widgets.editableText.readOnly} |
999 | /// Whether the text can be changed. |
1000 | /// |
1001 | /// When this is set to true, the text cannot be modified |
1002 | /// by any shortcut or keyboard operation. The text is still selectable. |
1003 | /// |
1004 | /// Defaults to false. |
1005 | /// {@endtemplate} |
1006 | final bool readOnly; |
1007 | |
1008 | /// Whether the text will take the full width regardless of the text width. |
1009 | /// |
1010 | /// When this is set to false, the width will be based on text width, which |
1011 | /// will also be affected by [textWidthBasis]. |
1012 | /// |
1013 | /// Defaults to true. |
1014 | /// |
1015 | /// See also: |
1016 | /// |
1017 | /// * [textWidthBasis], which controls the calculation of text width. |
1018 | final bool forceLine; |
1019 | |
1020 | /// Configuration of toolbar options. |
1021 | /// |
1022 | /// By default, all options are enabled. If [readOnly] is true, paste and cut |
1023 | /// will be disabled regardless. If [obscureText] is true, cut and copy will |
1024 | /// be disabled regardless. If [readOnly] and [obscureText] are both true, |
1025 | /// select all will also be disabled. |
1026 | final ToolbarOptions toolbarOptions; |
1027 | |
1028 | /// Whether to show selection handles. |
1029 | /// |
1030 | /// When a selection is active, there will be two handles at each side of |
1031 | /// boundary, or one handle if the selection is collapsed. The handles can be |
1032 | /// dragged to adjust the selection. |
1033 | /// |
1034 | /// See also: |
1035 | /// |
1036 | /// * [showCursor], which controls the visibility of the cursor. |
1037 | final bool showSelectionHandles; |
1038 | |
1039 | /// {@template flutter.widgets.editableText.showCursor} |
1040 | /// Whether to show cursor. |
1041 | /// |
1042 | /// The cursor refers to the blinking caret when the [EditableText] is focused. |
1043 | /// {@endtemplate} |
1044 | /// |
1045 | /// See also: |
1046 | /// |
1047 | /// * [showSelectionHandles], which controls the visibility of the selection handles. |
1048 | final bool showCursor; |
1049 | |
1050 | /// {@template flutter.widgets.editableText.autocorrect} |
1051 | /// Whether to enable autocorrection. |
1052 | /// |
1053 | /// Defaults to true. |
1054 | /// {@endtemplate} |
1055 | final bool autocorrect; |
1056 | |
1057 | /// {@macro flutter.services.TextInputConfiguration.smartDashesType} |
1058 | final SmartDashesType smartDashesType; |
1059 | |
1060 | /// {@macro flutter.services.TextInputConfiguration.smartQuotesType} |
1061 | final SmartQuotesType smartQuotesType; |
1062 | |
1063 | /// {@macro flutter.services.TextInputConfiguration.enableSuggestions} |
1064 | final bool enableSuggestions; |
1065 | |
1066 | /// The text style to use for the editable text. |
1067 | final TextStyle style; |
1068 | |
1069 | /// Controls the undo state of the current editable text. |
1070 | /// |
1071 | /// If null, this widget will create its own [UndoHistoryController]. |
1072 | final UndoHistoryController? undoController; |
1073 | |
1074 | /// {@template flutter.widgets.editableText.strutStyle} |
1075 | /// The strut style used for the vertical layout. |
1076 | /// |
1077 | /// [StrutStyle] is used to establish a predictable vertical layout. |
1078 | /// Since fonts may vary depending on user input and due to font |
1079 | /// fallback, [StrutStyle.forceStrutHeight] is enabled by default |
1080 | /// to lock all lines to the height of the base [TextStyle], provided by |
1081 | /// [style]. This ensures the typed text fits within the allotted space. |
1082 | /// |
1083 | /// If null, the strut used will inherit values from the [style] and will |
1084 | /// have [StrutStyle.forceStrutHeight] set to true. When no [style] is |
1085 | /// passed, the theme's [TextStyle] will be used to generate [strutStyle] |
1086 | /// instead. |
1087 | /// |
1088 | /// To disable strut-based vertical alignment and allow dynamic vertical |
1089 | /// layout based on the glyphs typed, use [StrutStyle.disabled]. |
1090 | /// |
1091 | /// Flutter's strut is based on [typesetting strut](https://en.wikipedia.org/wiki/Strut_(typesetting)) |
1092 | /// and CSS's [line-height](https://www.w3.org/TR/CSS2/visudet.html#line-height). |
1093 | /// {@endtemplate} |
1094 | /// |
1095 | /// Within editable text and text fields, [StrutStyle] will not use its standalone |
1096 | /// default values, and will instead inherit omitted/null properties from the |
1097 | /// [TextStyle] instead. See [StrutStyle.inheritFromTextStyle]. |
1098 | StrutStyle get strutStyle { |
1099 | if (_strutStyle == null) { |
1100 | return StrutStyle.fromTextStyle(style, forceStrutHeight: true); |
1101 | } |
1102 | return _strutStyle.inheritFromTextStyle(style); |
1103 | } |
1104 | |
1105 | final StrutStyle? _strutStyle; |
1106 | |
1107 | /// {@template flutter.widgets.editableText.textAlign} |
1108 | /// How the text should be aligned horizontally. |
1109 | /// |
1110 | /// Defaults to [TextAlign.start]. |
1111 | /// {@endtemplate} |
1112 | final TextAlign textAlign; |
1113 | |
1114 | /// {@template flutter.widgets.editableText.textDirection} |
1115 | /// The directionality of the text. |
1116 | /// |
1117 | /// This decides how [textAlign] values like [TextAlign.start] and |
1118 | /// [TextAlign.end] are interpreted. |
1119 | /// |
1120 | /// This is also used to disambiguate how to render bidirectional text. For |
1121 | /// example, if the text is an English phrase followed by a Hebrew phrase, |
1122 | /// in a [TextDirection.ltr] context the English phrase will be on the left |
1123 | /// and the Hebrew phrase to its right, while in a [TextDirection.rtl] |
1124 | /// context, the English phrase will be on the right and the Hebrew phrase on |
1125 | /// its left. |
1126 | /// |
1127 | /// Defaults to the ambient [Directionality], if any. |
1128 | /// {@endtemplate} |
1129 | final TextDirection? textDirection; |
1130 | |
1131 | /// {@template flutter.widgets.editableText.textCapitalization} |
1132 | /// Configures how the platform keyboard will select an uppercase or |
1133 | /// lowercase keyboard. |
1134 | /// |
1135 | /// Only supports text keyboards, other keyboard types will ignore this |
1136 | /// configuration. Capitalization is locale-aware. |
1137 | /// |
1138 | /// Defaults to [TextCapitalization.none]. |
1139 | /// |
1140 | /// See also: |
1141 | /// |
1142 | /// * [TextCapitalization], for a description of each capitalization behavior. |
1143 | /// |
1144 | /// {@endtemplate} |
1145 | final TextCapitalization textCapitalization; |
1146 | |
1147 | /// Used to select a font when the same Unicode character can |
1148 | /// be rendered differently, depending on the locale. |
1149 | /// |
1150 | /// It's rarely necessary to set this property. By default its value |
1151 | /// is inherited from the enclosing app with `Localizations.localeOf(context)`. |
1152 | /// |
1153 | /// See [RenderEditable.locale] for more information. |
1154 | final Locale? locale; |
1155 | |
1156 | /// {@template flutter.widgets.editableText.textScaleFactor} |
1157 | /// Deprecated. Will be removed in a future version of Flutter. Use |
1158 | /// [textScaler] instead. |
1159 | /// |
1160 | /// The number of font pixels for each logical pixel. |
1161 | /// |
1162 | /// For example, if the text scale factor is 1.5, text will be 50% larger than |
1163 | /// the specified font size. |
1164 | /// |
1165 | /// Defaults to the [MediaQueryData.textScaleFactor] obtained from the ambient |
1166 | /// [MediaQuery], or 1.0 if there is no [MediaQuery] in scope. |
1167 | /// {@endtemplate} |
1168 | @Deprecated( |
1169 | 'Use textScaler instead. ' |
1170 | 'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. ' |
1171 | 'This feature was deprecated after v3.12.0-2.0.pre.', |
1172 | ) |
1173 | final double? textScaleFactor; |
1174 | |
1175 | /// {@macro flutter.painting.textPainter.textScaler} |
1176 | final TextScaler? textScaler; |
1177 | |
1178 | /// The color to use when painting the cursor. |
1179 | final Color cursorColor; |
1180 | |
1181 | /// The color to use when painting the autocorrection Rect. |
1182 | /// |
1183 | /// For [CupertinoTextField]s, the value is set to the ambient |
1184 | /// [CupertinoThemeData.primaryColor] with 20% opacity. For [TextField]s, the |
1185 | /// value is null on non-iOS platforms and the same color used in [CupertinoTextField] |
1186 | /// on iOS. |
1187 | /// |
1188 | /// Currently the autocorrection Rect only appears on iOS. |
1189 | /// |
1190 | /// Defaults to null, which disables autocorrection Rect painting. |
1191 | final Color? autocorrectionTextRectColor; |
1192 | |
1193 | /// The color to use when painting the background cursor aligned with the text |
1194 | /// while rendering the floating cursor. |
1195 | /// |
1196 | /// Typically this would be set to [CupertinoColors.inactiveGray]. |
1197 | /// |
1198 | /// See also: |
1199 | /// |
1200 | /// * [FloatingCursorDragState], which explains the floating cursor feature |
1201 | /// in detail. |
1202 | final Color backgroundCursorColor; |
1203 | |
1204 | /// {@template flutter.widgets.editableText.maxLines} |
1205 | /// The maximum number of lines to show at one time, wrapping if necessary. |
1206 | /// |
1207 | /// This affects the height of the field itself and does not limit the number |
1208 | /// of lines that can be entered into the field. |
1209 | /// |
1210 | /// If this is 1 (the default), the text will not wrap, but will scroll |
1211 | /// horizontally instead. |
1212 | /// |
1213 | /// If this is null, there is no limit to the number of lines, and the text |
1214 | /// container will start with enough vertical space for one line and |
1215 | /// automatically grow to accommodate additional lines as they are entered, up |
1216 | /// to the height of its constraints. |
1217 | /// |
1218 | /// If this is not null, the value must be greater than zero, and it will lock |
1219 | /// the input to the given number of lines and take up enough horizontal space |
1220 | /// to accommodate that number of lines. Setting [minLines] as well allows the |
1221 | /// input to grow and shrink between the indicated range. |
1222 | /// |
1223 | /// The full set of behaviors possible with [minLines] and [maxLines] are as |
1224 | /// follows. These examples apply equally to [TextField], [TextFormField], |
1225 | /// [CupertinoTextField], and [EditableText]. |
1226 | /// |
1227 | /// Input that occupies a single line and scrolls horizontally as needed. |
1228 | /// ```dart |
1229 | /// const TextField() |
1230 | /// ``` |
1231 | /// |
1232 | /// Input whose height grows from one line up to as many lines as needed for |
1233 | /// the text that was entered. If a height limit is imposed by its parent, it |
1234 | /// will scroll vertically when its height reaches that limit. |
1235 | /// ```dart |
1236 | /// const TextField(maxLines: null) |
1237 | /// ``` |
1238 | /// |
1239 | /// The input's height is large enough for the given number of lines. If |
1240 | /// additional lines are entered the input scrolls vertically. |
1241 | /// ```dart |
1242 | /// const TextField(maxLines: 2) |
1243 | /// ``` |
1244 | /// |
1245 | /// Input whose height grows with content between a min and max. An infinite |
1246 | /// max is possible with `maxLines: null`. |
1247 | /// ```dart |
1248 | /// const TextField(minLines: 2, maxLines: 4) |
1249 | /// ``` |
1250 | /// |
1251 | /// See also: |
1252 | /// |
1253 | /// * [minLines], which sets the minimum number of lines visible. |
1254 | /// {@endtemplate} |
1255 | /// * [expands], which determines whether the field should fill the height of |
1256 | /// its parent. |
1257 | final int? maxLines; |
1258 | |
1259 | /// {@template flutter.widgets.editableText.minLines} |
1260 | /// The minimum number of lines to occupy when the content spans fewer lines. |
1261 | /// |
1262 | /// This affects the height of the field itself and does not limit the number |
1263 | /// of lines that can be entered into the field. |
1264 | /// |
1265 | /// If this is null (default), text container starts with enough vertical space |
1266 | /// for one line and grows to accommodate additional lines as they are entered. |
1267 | /// |
1268 | /// This can be used in combination with [maxLines] for a varying set of behaviors. |
1269 | /// |
1270 | /// If the value is set, it must be greater than zero. If the value is greater |
1271 | /// than 1, [maxLines] should also be set to either null or greater than |
1272 | /// this value. |
1273 | /// |
1274 | /// When [maxLines] is set as well, the height will grow between the indicated |
1275 | /// range of lines. When [maxLines] is null, it will grow as high as needed, |
1276 | /// starting from [minLines]. |
1277 | /// |
1278 | /// A few examples of behaviors possible with [minLines] and [maxLines] are as follows. |
1279 | /// These apply equally to [TextField], [TextFormField], [CupertinoTextField], |
1280 | /// and [EditableText]. |
1281 | /// |
1282 | /// Input that always occupies at least 2 lines and has an infinite max. |
1283 | /// Expands vertically as needed. |
1284 | /// ```dart |
1285 | /// TextField(minLines: 2) |
1286 | /// ``` |
1287 | /// |
1288 | /// Input whose height starts from 2 lines and grows up to 4 lines at which |
1289 | /// point the height limit is reached. If additional lines are entered it will |
1290 | /// scroll vertically. |
1291 | /// ```dart |
1292 | /// const TextField(minLines:2, maxLines: 4) |
1293 | /// ``` |
1294 | /// |
1295 | /// Defaults to null. |
1296 | /// |
1297 | /// See also: |
1298 | /// |
1299 | /// * [maxLines], which sets the maximum number of lines visible, and has |
1300 | /// several examples of how minLines and maxLines interact to produce |
1301 | /// various behaviors. |
1302 | /// {@endtemplate} |
1303 | /// * [expands], which determines whether the field should fill the height of |
1304 | /// its parent. |
1305 | final int? minLines; |
1306 | |
1307 | /// {@template flutter.widgets.editableText.expands} |
1308 | /// Whether this widget's height will be sized to fill its parent. |
1309 | /// |
1310 | /// If set to true and wrapped in a parent widget like [Expanded] or |
1311 | /// [SizedBox], the input will expand to fill the parent. |
1312 | /// |
1313 | /// [maxLines] and [minLines] must both be null when this is set to true, |
1314 | /// otherwise an error is thrown. |
1315 | /// |
1316 | /// Defaults to false. |
1317 | /// |
1318 | /// See the examples in [maxLines] for the complete picture of how [maxLines], |
1319 | /// [minLines], and [expands] interact to produce various behaviors. |
1320 | /// |
1321 | /// Input that matches the height of its parent: |
1322 | /// ```dart |
1323 | /// const Expanded( |
1324 | /// child: TextField(maxLines: null, expands: true), |
1325 | /// ) |
1326 | /// ``` |
1327 | /// {@endtemplate} |
1328 | final bool expands; |
1329 | |
1330 | /// {@template flutter.widgets.editableText.autofocus} |
1331 | /// Whether this text field should focus itself if nothing else is already |
1332 | /// focused. |
1333 | /// |
1334 | /// If true, the keyboard will open as soon as this text field obtains focus. |
1335 | /// Otherwise, the keyboard is only shown after the user taps the text field. |
1336 | /// |
1337 | /// Defaults to false. |
1338 | /// {@endtemplate} |
1339 | // See https://github.com/flutter/flutter/issues/7035 for the rationale for this |
1340 | // keyboard behavior. |
1341 | final bool autofocus; |
1342 | |
1343 | /// The color to use when painting the selection. |
1344 | /// |
1345 | /// If this property is null, this widget gets the selection color from the |
1346 | /// [DefaultSelectionStyle]. |
1347 | /// |
1348 | /// For [CupertinoTextField]s, the value is set to the ambient |
1349 | /// [CupertinoThemeData.primaryColor] with 20% opacity. For [TextField]s, the |
1350 | /// value is set to the ambient [TextSelectionThemeData.selectionColor]. |
1351 | final Color? selectionColor; |
1352 | |
1353 | /// {@template flutter.widgets.editableText.selectionControls} |
1354 | /// Optional delegate for building the text selection handles. |
1355 | /// |
1356 | /// Historically, this field also controlled the toolbar. This is now handled |
1357 | /// by [contextMenuBuilder] instead. However, for backwards compatibility, when |
1358 | /// [selectionControls] is set to an object that does not mix in |
1359 | /// [TextSelectionHandleControls], [contextMenuBuilder] is ignored and the |
1360 | /// [TextSelectionControls.buildToolbar] method is used instead. |
1361 | /// {@endtemplate} |
1362 | /// |
1363 | /// See also: |
1364 | /// |
1365 | /// * [CupertinoTextField], which wraps an [EditableText] and which shows the |
1366 | /// selection toolbar upon user events that are appropriate on the iOS |
1367 | /// platform. |
1368 | /// * [TextField], a Material Design themed wrapper of [EditableText], which |
1369 | /// shows the selection toolbar upon appropriate user events based on the |
1370 | /// user's platform set in [ThemeData.platform]. |
1371 | final TextSelectionControls? selectionControls; |
1372 | |
1373 | /// {@template flutter.widgets.editableText.keyboardType} |
1374 | /// The type of keyboard to use for editing the text. |
1375 | /// |
1376 | /// Defaults to [TextInputType.text] if [maxLines] is one and |
1377 | /// [TextInputType.multiline] otherwise. |
1378 | /// {@endtemplate} |
1379 | final TextInputType keyboardType; |
1380 | |
1381 | /// The type of action button to use with the soft keyboard. |
1382 | final TextInputAction? textInputAction; |
1383 | |
1384 | /// {@template flutter.widgets.editableText.onChanged} |
1385 | /// Called when the user initiates a change to the TextField's |
1386 | /// value: when they have inserted or deleted text. |
1387 | /// |
1388 | /// This callback doesn't run when the TextField's text is changed |
1389 | /// programmatically, via the TextField's [controller]. Typically it |
1390 | /// isn't necessary to be notified of such changes, since they're |
1391 | /// initiated by the app itself. |
1392 | /// |
1393 | /// To be notified of all changes to the TextField's text, cursor, |
1394 | /// and selection, one can add a listener to its [controller] with |
1395 | /// [TextEditingController.addListener]. |
1396 | /// |
1397 | /// [onChanged] is called before [onSubmitted] when user indicates completion |
1398 | /// of editing, such as when pressing the "done" button on the keyboard. That |
1399 | /// default behavior can be overridden. See [onEditingComplete] for details. |
1400 | /// |
1401 | /// {@tool dartpad} |
1402 | /// This example shows how onChanged could be used to check the TextField's |
1403 | /// current value each time the user inserts or deletes a character. |
1404 | /// |
1405 | /// ** See code in examples/api/lib/widgets/editable_text/editable_text.on_changed.0.dart ** |
1406 | /// {@end-tool} |
1407 | /// {@endtemplate} |
1408 | /// |
1409 | /// ## Handling emojis and other complex characters |
1410 | /// {@template flutter.widgets.EditableText.onChanged} |
1411 | /// It's important to always use |
1412 | /// [characters](https://pub.dev/packages/characters) when dealing with user |
1413 | /// input text that may contain complex characters. This will ensure that |
1414 | /// extended grapheme clusters and surrogate pairs are treated as single |
1415 | /// characters, as they appear to the user. |
1416 | /// |
1417 | /// For example, when finding the length of some user input, use |
1418 | /// `string.characters.length`. Do NOT use `string.length` or even |
1419 | /// `string.runes.length`. For the complex character "👨👩👦", this |
1420 | /// appears to the user as a single character, and `string.characters.length` |
1421 | /// intuitively returns 1. On the other hand, `string.length` returns 8, and |
1422 | /// `string.runes.length` returns 5! |
1423 | /// {@endtemplate} |
1424 | /// |
1425 | /// See also: |
1426 | /// |
1427 | /// * [inputFormatters], which are called before [onChanged] |
1428 | /// runs and can validate and change ("format") the input value. |
1429 | /// * [onEditingComplete], [onSubmitted], [onSelectionChanged]: |
1430 | /// which are more specialized input change notifications. |
1431 | /// * [TextEditingController], which implements the [Listenable] interface |
1432 | /// and notifies its listeners on [TextEditingValue] changes. |
1433 | final ValueChanged<String>? onChanged; |
1434 | |
1435 | /// {@template flutter.widgets.editableText.onEditingComplete} |
1436 | /// Called when the user submits editable content (e.g., user presses the "done" |
1437 | /// button on the keyboard). |
1438 | /// |
1439 | /// The default implementation of [onEditingComplete] executes 2 different |
1440 | /// behaviors based on the situation: |
1441 | /// |
1442 | /// - When a completion action is pressed, such as "done", "go", "send", or |
1443 | /// "search", the user's content is submitted to the [controller] and then |
1444 | /// focus is given up. |
1445 | /// |
1446 | /// - When a non-completion action is pressed, such as "next" or "previous", |
1447 | /// the user's content is submitted to the [controller], but focus is not |
1448 | /// given up because developers may want to immediately move focus to |
1449 | /// another input widget within [onSubmitted]. |
1450 | /// |
1451 | /// Providing [onEditingComplete] prevents the aforementioned default behavior. |
1452 | /// {@endtemplate} |
1453 | final VoidCallback? onEditingComplete; |
1454 | |
1455 | /// {@template flutter.widgets.editableText.onSubmitted} |
1456 | /// Called when the user indicates that they are done editing the text in the |
1457 | /// field. |
1458 | /// |
1459 | /// By default, [onSubmitted] is called after [onChanged] when the user |
1460 | /// has finalized editing; or, if the default behavior has been overridden, |
1461 | /// after [onEditingComplete]. See [onEditingComplete] for details. |
1462 | /// |
1463 | /// ## Testing |
1464 | /// The following is the recommended way to trigger [onSubmitted] in a test: |
1465 | /// |
1466 | /// ```dart |
1467 | /// await tester.testTextInput.receiveAction(TextInputAction.done); |
1468 | /// ``` |
1469 | /// |
1470 | /// Sending a `LogicalKeyboardKey.enter` via `tester.sendKeyEvent` will not |
1471 | /// trigger [onSubmitted]. This is because on a real device, the engine |
1472 | /// translates the enter key to a done action, but `tester.sendKeyEvent` sends |
1473 | /// the key to the framework only. |
1474 | /// {@endtemplate} |
1475 | final ValueChanged<String>? onSubmitted; |
1476 | |
1477 | /// {@template flutter.widgets.editableText.onAppPrivateCommand} |
1478 | /// This is used to receive a private command from the input method. |
1479 | /// |
1480 | /// Called when the result of [TextInputClient.performPrivateCommand] is |
1481 | /// received. |
1482 | /// |
1483 | /// This can be used to provide domain-specific features that are only known |
1484 | /// between certain input methods and their clients. |
1485 | /// |
1486 | /// See also: |
1487 | /// * [performPrivateCommand](https://developer.android.com/reference/android/view/inputmethod/InputConnection#performPrivateCommand\(java.lang.String,%20android.os.Bundle\)), |
1488 | /// which is the Android documentation for performPrivateCommand, used to |
1489 | /// send a command from the input method. |
1490 | /// * [sendAppPrivateCommand](https://developer.android.com/reference/android/view/inputmethod/InputMethodManager#sendAppPrivateCommand), |
1491 | /// which is the Android documentation for sendAppPrivateCommand, used to |
1492 | /// send a command to the input method. |
1493 | /// {@endtemplate} |
1494 | final AppPrivateCommandCallback? onAppPrivateCommand; |
1495 | |
1496 | /// {@template flutter.widgets.editableText.onSelectionChanged} |
1497 | /// Called when the user changes the selection of text (including the cursor |
1498 | /// location). |
1499 | /// {@endtemplate} |
1500 | final SelectionChangedCallback? onSelectionChanged; |
1501 | |
1502 | /// {@macro flutter.widgets.SelectionOverlay.onSelectionHandleTapped} |
1503 | final VoidCallback? onSelectionHandleTapped; |
1504 | |
1505 | /// {@template flutter.widgets.editableText.groupId} |
1506 | /// The group identifier for the [TextFieldTapRegion] of this text field. |
1507 | /// |
1508 | /// Text fields with the same group identifier share the same tap region. |
1509 | /// Defaults to the type of [EditableText]. |
1510 | /// |
1511 | /// See also: |
1512 | /// |
1513 | /// * [TextFieldTapRegion], to give a [groupId] to a widget that is to be |
1514 | /// included in a [EditableText]'s tap region that has [groupId] set. |
1515 | /// {@endtemplate} |
1516 | final Object groupId; |
1517 | |
1518 | /// {@template flutter.widgets.editableText.onTapOutside} |
1519 | /// Called for each tap down that occurs outside of the [TextFieldTapRegion] |
1520 | /// group when the text field is focused. |
1521 | /// |
1522 | /// If this is null, [EditableTextTapOutsideIntent] will be invoked. In the |
1523 | /// default implementation, [FocusNode.unfocus] will be called on the |
1524 | /// [focusNode] for this text field when a [PointerDownEvent] is received on |
1525 | /// another part of the UI. However, it will not unfocus as a result of mobile |
1526 | /// application touch events (which does not include mouse clicks), to conform |
1527 | /// with the platform conventions. To change this behavior, a callback may be |
1528 | /// set here or [EditableTextTapOutsideIntent] may be overridden. |
1529 | /// |
1530 | /// When adding additional controls to a text field (for example, a spinner, a |
1531 | /// button that copies the selected text, or modifies formatting), it is |
1532 | /// helpful if tapping on that control doesn't unfocus the text field. In |
1533 | /// order for an external widget to be considered as part of the text field |
1534 | /// for the purposes of tapping "outside" of the field, wrap the control in a |
1535 | /// [TextFieldTapRegion]. |
1536 | /// |
1537 | /// The [PointerDownEvent] passed to the function is the event that caused the |
1538 | /// notification. It is possible that the event may occur outside of the |
1539 | /// immediate bounding box defined by the text field, although it will be |
1540 | /// within the bounding box of a [TextFieldTapRegion] member. |
1541 | /// {@endtemplate} |
1542 | /// |
1543 | /// {@tool dartpad} |
1544 | /// This example shows how to use a `TextFieldTapRegion` to wrap a set of |
1545 | /// "spinner" buttons that increment and decrement a value in the [TextField] |
1546 | /// without causing the text field to lose keyboard focus. |
1547 | /// |
1548 | /// This example includes a generic `SpinnerField<T>` class that you can copy |
1549 | /// into your own project and customize. |
1550 | /// |
1551 | /// ** See code in examples/api/lib/widgets/tap_region/text_field_tap_region.0.dart ** |
1552 | /// {@end-tool} |
1553 | /// |
1554 | /// See also: |
1555 | /// |
1556 | /// * [TapRegion] for how the region group is determined. |
1557 | /// * [onTapUpOutside] which is called for each tap up. |
1558 | /// * [EditableTextTapOutsideIntent] for the intent that is invoked if |
1559 | /// this is null. |
1560 | final TapRegionCallback? onTapOutside; |
1561 | |
1562 | /// {@template flutter.widgets.editableText.onTapUpOutside} |
1563 | /// Called for each tap up that occurs outside of the [TextFieldTapRegion] |
1564 | /// group when the text field is focused. |
1565 | /// |
1566 | /// If this is null, [EditableTextTapUpOutsideIntent] will be invoked. In the |
1567 | /// default implementation, this is a no-op. To change this behavior, set a |
1568 | /// callback here or override [EditableTextTapUpOutsideIntent]. |
1569 | /// |
1570 | /// The [PointerUpEvent] passed to the function is the event that caused the |
1571 | /// notification. It is possible that the event may occur outside of the |
1572 | /// immediate bounding box defined by the text field, although it will be |
1573 | /// within the bounding box of a [TextFieldTapRegion] member. |
1574 | /// {@endtemplate} |
1575 | /// |
1576 | /// See also: |
1577 | /// |
1578 | /// * [TapRegion] for how the region group is determined. |
1579 | /// * [onTapOutside], which is called for each tap down. |
1580 | /// * [EditableTextTapOutsideIntent], the intent that is invoked if |
1581 | /// this is null. |
1582 | final TapRegionUpCallback? onTapUpOutside; |
1583 | |
1584 | /// {@template flutter.widgets.editableText.inputFormatters} |
1585 | /// Optional input validation and formatting overrides. |
1586 | /// |
1587 | /// Formatters are run in the provided order when the user changes the text |
1588 | /// this widget contains. When this parameter changes, the new formatters will |
1589 | /// not be applied until the next time the user inserts or deletes text. |
1590 | /// Similar to the [onChanged] callback, formatters don't run when the text is |
1591 | /// changed programmatically via [controller]. |
1592 | /// |
1593 | /// See also: |
1594 | /// |
1595 | /// * [TextEditingController], which implements the [Listenable] interface |
1596 | /// and notifies its listeners on [TextEditingValue] changes. |
1597 | /// {@endtemplate} |
1598 | final List<TextInputFormatter>? inputFormatters; |
1599 | |
1600 | /// The cursor for a mouse pointer when it enters or is hovering over the |
1601 | /// widget. |
1602 | /// |
1603 | /// If this property is null, [SystemMouseCursors.text] will be used. |
1604 | /// |
1605 | /// The [mouseCursor] is the only property of [EditableText] that controls the |
1606 | /// appearance of the mouse pointer. All other properties related to "cursor" |
1607 | /// stands for the text cursor, which is usually a blinking vertical line at |
1608 | /// the editing position. |
1609 | final MouseCursor? mouseCursor; |
1610 | |
1611 | /// Whether the caller will provide gesture handling (true), or if the |
1612 | /// [EditableText] is expected to handle basic gestures (false). |
1613 | /// |
1614 | /// When this is false, the [EditableText] (or more specifically, the |
1615 | /// [RenderEditable]) enables some rudimentary gestures (tap to position the |
1616 | /// cursor, long-press to select all, and some scrolling behavior). |
1617 | /// |
1618 | /// These behaviors are sufficient for debugging purposes but are inadequate |
1619 | /// for user-facing applications. To enable platform-specific behaviors, use a |
1620 | /// [TextSelectionGestureDetectorBuilder] to wrap the [EditableText], and set |
1621 | /// [rendererIgnoresPointer] to true. |
1622 | /// |
1623 | /// When [rendererIgnoresPointer] is true true, the [RenderEditable] created |
1624 | /// by this widget will not handle pointer events. |
1625 | /// |
1626 | /// This property is false by default. |
1627 | /// |
1628 | /// See also: |
1629 | /// |
1630 | /// * [RenderEditable.ignorePointer], which implements this feature. |
1631 | /// * [TextSelectionGestureDetectorBuilder], which implements platform-specific |
1632 | /// gestures and behaviors. |
1633 | final bool rendererIgnoresPointer; |
1634 | |
1635 | /// {@template flutter.widgets.editableText.cursorWidth} |
1636 | /// How thick the cursor will be. |
1637 | /// |
1638 | /// Defaults to 2.0. |
1639 | /// |
1640 | /// The cursor will draw under the text. The cursor width will extend |
1641 | /// to the right of the boundary between characters for left-to-right text |
1642 | /// and to the left for right-to-left text. This corresponds to extending |
1643 | /// downstream relative to the selected position. Negative values may be used |
1644 | /// to reverse this behavior. |
1645 | /// {@endtemplate} |
1646 | final double cursorWidth; |
1647 | |
1648 | /// {@template flutter.widgets.editableText.cursorHeight} |
1649 | /// How tall the cursor will be. |
1650 | /// |
1651 | /// If this property is null, [RenderEditable.preferredLineHeight] will be used. |
1652 | /// {@endtemplate} |
1653 | final double? cursorHeight; |
1654 | |
1655 | /// {@template flutter.widgets.editableText.cursorRadius} |
1656 | /// How rounded the corners of the cursor should be. |
1657 | /// |
1658 | /// By default, the cursor has no radius. |
1659 | /// {@endtemplate} |
1660 | final Radius? cursorRadius; |
1661 | |
1662 | /// {@template flutter.widgets.editableText.cursorOpacityAnimates} |
1663 | /// Whether the cursor will animate from fully transparent to fully opaque |
1664 | /// during each cursor blink. |
1665 | /// |
1666 | /// By default, the cursor opacity will animate on iOS platforms and will not |
1667 | /// animate on Android platforms. |
1668 | /// {@endtemplate} |
1669 | final bool cursorOpacityAnimates; |
1670 | |
1671 | /// {@macro flutter.rendering.RenderEditable.cursorOffset} |
1672 | final Offset? cursorOffset; |
1673 | |
1674 | /// {@macro flutter.rendering.RenderEditable.paintCursorAboveText} |
1675 | final bool paintCursorAboveText; |
1676 | |
1677 | /// Controls how tall the selection highlight boxes are computed to be. |
1678 | /// |
1679 | /// See [ui.BoxHeightStyle] for details on available styles. |
1680 | final ui.BoxHeightStyle selectionHeightStyle; |
1681 | |
1682 | /// Controls how wide the selection highlight boxes are computed to be. |
1683 | /// |
1684 | /// See [ui.BoxWidthStyle] for details on available styles. |
1685 | final ui.BoxWidthStyle selectionWidthStyle; |
1686 | |
1687 | /// The appearance of the keyboard. |
1688 | /// |
1689 | /// This setting is only honored on iOS devices. |
1690 | /// |
1691 | /// Defaults to [Brightness.light]. |
1692 | final Brightness keyboardAppearance; |
1693 | |
1694 | /// {@template flutter.widgets.editableText.scrollPadding} |
1695 | /// Configures the padding for the edges surrounding a [Scrollable] when the |
1696 | /// text field scrolls into view. |
1697 | /// |
1698 | /// When this widget receives focus and is not completely visible (for example |
1699 | /// scrolled partially off the screen or overlapped by the keyboard), then it |
1700 | /// will attempt to make itself visible by scrolling a surrounding |
1701 | /// [Scrollable], if one is present. This value controls how far from the |
1702 | /// edges of a [Scrollable] the TextField will be positioned after the scroll. |
1703 | /// |
1704 | /// Defaults to EdgeInsets.all(20.0). |
1705 | /// {@endtemplate} |
1706 | final EdgeInsets scrollPadding; |
1707 | |
1708 | /// {@template flutter.widgets.editableText.enableInteractiveSelection} |
1709 | /// Whether to enable user interface affordances for changing the |
1710 | /// text selection. |
1711 | /// |
1712 | /// For example, setting this to true will enable features such as |
1713 | /// long-pressing the TextField to select text and show the |
1714 | /// cut/copy/paste menu, and tapping to move the text caret. |
1715 | /// |
1716 | /// When this is false, the text selection cannot be adjusted by |
1717 | /// the user, text cannot be copied, and the user cannot paste into |
1718 | /// the text field from the clipboard. |
1719 | /// |
1720 | /// Defaults to true. |
1721 | /// {@endtemplate} |
1722 | final bool enableInteractiveSelection; |
1723 | |
1724 | /// Setting this property to true makes the cursor stop blinking or fading |
1725 | /// on and off once the cursor appears on focus. This property is useful for |
1726 | /// testing purposes. |
1727 | /// |
1728 | /// It does not affect the necessity to focus the EditableText for the cursor |
1729 | /// to appear in the first place. |
1730 | /// |
1731 | /// Defaults to false, resulting in a typical blinking cursor. |
1732 | static bool debugDeterministicCursor = false; |
1733 | |
1734 | /// {@macro flutter.widgets.scrollable.dragStartBehavior} |
1735 | final DragStartBehavior dragStartBehavior; |
1736 | |
1737 | /// {@template flutter.widgets.editableText.scrollController} |
1738 | /// The [ScrollController] to use when vertically scrolling the input. |
1739 | /// |
1740 | /// If null, it will instantiate a new ScrollController. |
1741 | /// |
1742 | /// See [Scrollable.controller]. |
1743 | /// {@endtemplate} |
1744 | final ScrollController? scrollController; |
1745 | |
1746 | /// {@template flutter.widgets.editableText.scrollPhysics} |
1747 | /// The [ScrollPhysics] to use when vertically scrolling the input. |
1748 | /// |
1749 | /// If not specified, it will behave according to the current platform. |
1750 | /// |
1751 | /// See [Scrollable.physics]. |
1752 | /// {@endtemplate} |
1753 | /// |
1754 | /// If an explicit [ScrollBehavior] is provided to [scrollBehavior], the |
1755 | /// [ScrollPhysics] provided by that behavior will take precedence after |
1756 | /// [scrollPhysics]. |
1757 | final ScrollPhysics? scrollPhysics; |
1758 | |
1759 | /// {@template flutter.widgets.editableText.scribbleEnabled} |
1760 | /// Whether iOS 14 Scribble features are enabled for this widget. |
1761 | /// |
1762 | /// Only available on iPads. |
1763 | /// |
1764 | /// Defaults to true. |
1765 | /// {@endtemplate} |
1766 | @Deprecated( |
1767 | 'Use `stylusHandwritingEnabled` instead. ' |
1768 | 'This feature was deprecated after v3.27.0-0.2.pre.', |
1769 | ) |
1770 | final bool scribbleEnabled; |
1771 | |
1772 | /// {@template flutter.widgets.editableText.stylusHandwritingEnabled} |
1773 | /// Whether this input supports stylus handwriting, where the user can write |
1774 | /// directly on top of a field. |
1775 | /// |
1776 | /// Currently only the following devices are supported: |
1777 | /// |
1778 | /// * iPads running iOS 14 and above using an Apple Pencil. |
1779 | /// * Android devices running API 34 and above and using an active stylus. |
1780 | /// {@endtemplate} |
1781 | /// |
1782 | /// On Android, Scribe gestures are detected outside of [EditableText], |
1783 | /// typically by [TextSelectionGestureDetectorBuilder]. This is handled |
1784 | /// automatically in [TextField]. |
1785 | /// |
1786 | /// See also: |
1787 | /// |
1788 | /// * [ScribbleClient], which can be mixed into an arbitrary widget to |
1789 | /// provide iOS Scribble functionality. |
1790 | /// * [Scribe], which can be used to interact with Android Scribe directly. |
1791 | final bool stylusHandwritingEnabled; |
1792 | |
1793 | /// {@template flutter.widgets.editableText.selectionEnabled} |
1794 | /// Same as [enableInteractiveSelection]. |
1795 | /// |
1796 | /// This getter exists primarily for consistency with |
1797 | /// [RenderEditable.selectionEnabled]. |
1798 | /// {@endtemplate} |
1799 | bool get selectionEnabled => enableInteractiveSelection; |
1800 | |
1801 | /// {@template flutter.widgets.editableText.autofillHints} |
1802 | /// A list of strings that helps the autofill service identify the type of this |
1803 | /// text input. |
1804 | /// |
1805 | /// When set to null, this text input will not send its autofill information |
1806 | /// to the platform, preventing it from participating in autofills triggered |
1807 | /// by a different [AutofillClient], even if they're in the same |
1808 | /// [AutofillScope]. Additionally, on Android and web, setting this to null |
1809 | /// will disable autofill for this text field. |
1810 | /// |
1811 | /// The minimum platform SDK version that supports Autofill is API level 26 |
1812 | /// for Android, and iOS 10.0 for iOS. |
1813 | /// |
1814 | /// Defaults to an empty list. |
1815 | /// |
1816 | /// ### Setting up iOS autofill: |
1817 | /// |
1818 | /// To provide the best user experience and ensure your app fully supports |
1819 | /// password autofill on iOS, follow these steps: |
1820 | /// |
1821 | /// * Set up your iOS app's |
1822 | /// [associated domains](https://developer.apple.com/documentation/safariservices/supporting_associated_domains_in_your_app). |
1823 | /// * Some autofill hints only work with specific [keyboardType]s. For example, |
1824 | /// [AutofillHints.name] requires [TextInputType.name] and [AutofillHints.email] |
1825 | /// works only with [TextInputType.emailAddress]. Make sure the input field has a |
1826 | /// compatible [keyboardType]. Empirically, [TextInputType.name] works well |
1827 | /// with many autofill hints that are predefined on iOS. |
1828 | /// |
1829 | /// ### Troubleshooting Autofill |
1830 | /// |
1831 | /// Autofill service providers rely heavily on [autofillHints]. Make sure the |
1832 | /// entries in [autofillHints] are supported by the autofill service currently |
1833 | /// in use (the name of the service can typically be found in your mobile |
1834 | /// device's system settings). |
1835 | /// |
1836 | /// #### Autofill UI refuses to show up when I tap on the text field |
1837 | /// |
1838 | /// Check the device's system settings and make sure autofill is turned on, |
1839 | /// and there are available credentials stored in the autofill service. |
1840 | /// |
1841 | /// * iOS password autofill: Go to Settings -> Password, turn on "Autofill |
1842 | /// Passwords", and add new passwords for testing by pressing the top right |
1843 | /// "+" button. Use an arbitrary "website" if you don't have associated |
1844 | /// domains set up for your app. As long as there's at least one password |
1845 | /// stored, you should be able to see a key-shaped icon in the quick type |
1846 | /// bar on the software keyboard, when a password related field is focused. |
1847 | /// |
1848 | /// * iOS contact information autofill: iOS seems to pull contact info from |
1849 | /// the Apple ID currently associated with the device. Go to Settings -> |
1850 | /// Apple ID (usually the first entry, or "Sign in to your iPhone" if you |
1851 | /// haven't set up one on the device), and fill out the relevant fields. If |
1852 | /// you wish to test more contact info types, try adding them in Contacts -> |
1853 | /// My Card. |
1854 | /// |
1855 | /// * Android autofill: Go to Settings -> System -> Languages & input -> |
1856 | /// Autofill service. Enable the autofill service of your choice, and make |
1857 | /// sure there are available credentials associated with your app. |
1858 | /// |
1859 | /// Specifying [InputDecoration.hintText] may also help autofill services |
1860 | /// (like Samsung Pass) determine the expected content type of an input field, |
1861 | /// although this is typically not required when autofillHints are present. |
1862 | /// |
1863 | /// #### I called `TextInput.finishAutofillContext` but the autofill save |
1864 | /// prompt isn't showing |
1865 | /// |
1866 | /// * iOS: iOS may not show a prompt or any other visual indication when it |
1867 | /// saves user password. Go to Settings -> Password and check if your new |
1868 | /// password is saved. Neither saving password nor auto-generating strong |
1869 | /// password works without properly setting up associated domains in your |
1870 | /// app. To set up associated domains, follow the instructions in |
1871 | /// <https://developer.apple.com/documentation/safariservices/supporting_associated_domains_in_your_app>. |
1872 | /// |
1873 | /// {@endtemplate} |
1874 | /// {@macro flutter.services.AutofillConfiguration.autofillHints} |
1875 | final Iterable<String>? autofillHints; |
1876 | |
1877 | /// The [AutofillClient] that controls this input field's autofill behavior. |
1878 | /// |
1879 | /// When null, this widget's [EditableTextState] will be used as the |
1880 | /// [AutofillClient]. This property may override [autofillHints]. |
1881 | final AutofillClient? autofillClient; |
1882 | |
1883 | /// {@macro flutter.material.Material.clipBehavior} |
1884 | /// |
1885 | /// Defaults to [Clip.hardEdge]. |
1886 | final Clip clipBehavior; |
1887 | |
1888 | /// Restoration ID to save and restore the scroll offset of the |
1889 | /// [EditableText]. |
1890 | /// |
1891 | /// If a restoration id is provided, the [EditableText] will persist its |
1892 | /// current scroll offset and restore it during state restoration. |
1893 | /// |
1894 | /// The scroll offset is persisted in a [RestorationBucket] claimed from |
1895 | /// the surrounding [RestorationScope] using the provided restoration ID. |
1896 | /// |
1897 | /// Persisting and restoring the content of the [EditableText] is the |
1898 | /// responsibility of the owner of the [controller], who may use a |
1899 | /// [RestorableTextEditingController] for that purpose. |
1900 | /// |
1901 | /// See also: |
1902 | /// |
1903 | /// * [RestorationManager], which explains how state restoration works in |
1904 | /// Flutter. |
1905 | final String? restorationId; |
1906 | |
1907 | /// {@template flutter.widgets.editableText.scrollBehavior} |
1908 | /// A [ScrollBehavior] that will be applied to this widget individually. |
1909 | /// |
1910 | /// Defaults to null, wherein the inherited [ScrollBehavior] is copied and |
1911 | /// modified to alter the viewport decoration, like [Scrollbar]s. |
1912 | /// |
1913 | /// [ScrollBehavior]s also provide [ScrollPhysics]. If an explicit |
1914 | /// [ScrollPhysics] is provided in [scrollPhysics], it will take precedence, |
1915 | /// followed by [scrollBehavior], and then the inherited ancestor |
1916 | /// [ScrollBehavior]. |
1917 | /// {@endtemplate} |
1918 | /// |
1919 | /// The [ScrollBehavior] of the inherited [ScrollConfiguration] will be |
1920 | /// modified by default to only apply a [Scrollbar] if [maxLines] is greater |
1921 | /// than 1. |
1922 | final ScrollBehavior? scrollBehavior; |
1923 | |
1924 | /// {@macro flutter.services.TextInputConfiguration.enableIMEPersonalizedLearning} |
1925 | final bool enableIMEPersonalizedLearning; |
1926 | |
1927 | /// {@template flutter.widgets.editableText.contentInsertionConfiguration} |
1928 | /// Configuration of handler for media content inserted via the system input |
1929 | /// method. |
1930 | /// |
1931 | /// Defaults to null in which case media content insertion will be disabled, |
1932 | /// and the system will display a message informing the user that the text field |
1933 | /// does not support inserting media content. |
1934 | /// |
1935 | /// Set [ContentInsertionConfiguration.onContentInserted] to provide a handler. |
1936 | /// Additionally, set [ContentInsertionConfiguration.allowedMimeTypes] |
1937 | /// to limit the allowable mime types for inserted content. |
1938 | /// |
1939 | /// {@tool dartpad} |
1940 | /// |
1941 | /// This example shows how to access the data for inserted content in your |
1942 | /// `TextField`. |
1943 | /// |
1944 | /// ** See code in examples/api/lib/widgets/editable_text/editable_text.on_content_inserted.0.dart ** |
1945 | /// {@end-tool} |
1946 | /// |
1947 | /// If [contentInsertionConfiguration] is not provided, by default |
1948 | /// an empty list of mime types will be sent to the Flutter Engine. |
1949 | /// A handler function must be provided in order to customize the allowable |
1950 | /// mime types for inserted content. |
1951 | /// |
1952 | /// If rich content is inserted without a handler, the system will display |
1953 | /// a message informing the user that the current text input does not support |
1954 | /// inserting rich content. |
1955 | /// {@endtemplate} |
1956 | final ContentInsertionConfiguration? contentInsertionConfiguration; |
1957 | |
1958 | /// {@template flutter.widgets.EditableText.contextMenuBuilder} |
1959 | /// Builds the text selection toolbar when requested by the user. |
1960 | /// |
1961 | /// The context menu is built when [EditableTextState.showToolbar] is called, |
1962 | /// typically by one of the callbacks installed by the widget created by |
1963 | /// [TextSelectionGestureDetectorBuilder.buildGestureDetector]. The widget |
1964 | /// returned by [contextMenuBuilder] is passed to a [ContextMenuController]. |
1965 | /// |
1966 | /// If no callback is provided, no context menu will be shown. |
1967 | /// |
1968 | /// The [EditableTextContextMenuBuilder] signature used by the |
1969 | /// [contextMenuBuilder] callback has two parameters, the [BuildContext] of |
1970 | /// the [EditableText] and the [EditableTextState] of the [EditableText]. |
1971 | /// |
1972 | /// The [EditableTextState] has two properties that are especially useful when |
1973 | /// building the widgets for the context menu: |
1974 | /// |
1975 | /// * [EditableTextState.contextMenuAnchors] specifies the desired anchor |
1976 | /// position for the context menu. |
1977 | /// |
1978 | /// * [EditableTextState.contextMenuButtonItems] represents the buttons that |
1979 | /// should typically be built for this widget (e.g. cut, copy, paste). |
1980 | /// |
1981 | /// The [TextSelectionToolbarLayoutDelegate] class may be particularly useful |
1982 | /// in honoring the preferred anchor positions. |
1983 | /// |
1984 | /// For backwards compatibility, when [EditableText.selectionControls] is set |
1985 | /// to an object that does not mix in [TextSelectionHandleControls], |
1986 | /// [contextMenuBuilder] is ignored and the |
1987 | /// [TextSelectionControls.buildToolbar] method is used instead. |
1988 | /// |
1989 | /// {@tool dartpad} |
1990 | /// This example shows how to customize the menu, in this case by keeping the |
1991 | /// default buttons for the platform but modifying their appearance. |
1992 | /// |
1993 | /// ** See code in examples/api/lib/material/context_menu/editable_text_toolbar_builder.0.dart ** |
1994 | /// {@end-tool} |
1995 | /// |
1996 | /// {@tool dartpad} |
1997 | /// This example shows how to show a custom button only when an email address |
1998 | /// is currently selected. |
1999 | /// |
2000 | /// ** See code in examples/api/lib/material/context_menu/editable_text_toolbar_builder.1.dart ** |
2001 | /// {@end-tool} |
2002 | /// |
2003 | /// See also: |
2004 | /// * [AdaptiveTextSelectionToolbar], which builds the default text selection |
2005 | /// toolbar for the current platform, but allows customization of the |
2006 | /// buttons. |
2007 | /// * [AdaptiveTextSelectionToolbar.getAdaptiveButtons], which builds the |
2008 | /// button Widgets for the current platform given |
2009 | /// [ContextMenuButtonItem]s. |
2010 | /// * [BrowserContextMenu], which allows the browser's context menu on web |
2011 | /// to be disabled and Flutter-rendered context menus to appear. |
2012 | /// {@endtemplate} |
2013 | final EditableTextContextMenuBuilder? contextMenuBuilder; |
2014 | |
2015 | /// {@template flutter.widgets.EditableText.spellCheckConfiguration} |
2016 | /// Configuration that details how spell check should be performed. |
2017 | /// |
2018 | /// Specifies the [SpellCheckService] used to spell check text input and the |
2019 | /// [TextStyle] used to style text with misspelled words. |
2020 | /// |
2021 | /// If the [SpellCheckService] is left null, spell check is disabled by |
2022 | /// default unless the [DefaultSpellCheckService] is supported, in which case |
2023 | /// it is used. It is currently supported only on Android and iOS. |
2024 | /// |
2025 | /// If this configuration is left null, then spell check is disabled by default. |
2026 | /// {@endtemplate} |
2027 | final SpellCheckConfiguration? spellCheckConfiguration; |
2028 | |
2029 | /// The configuration for the magnifier to use with selections in this text |
2030 | /// field. |
2031 | /// |
2032 | /// {@macro flutter.widgets.magnifier.intro} |
2033 | final TextMagnifierConfiguration magnifierConfiguration; |
2034 | |
2035 | /// The default value for [stylusHandwritingEnabled]. |
2036 | static const bool defaultStylusHandwritingEnabled = true; |
2037 | |
2038 | bool get _userSelectionEnabled => enableInteractiveSelection && (!readOnly || !obscureText); |
2039 | |
2040 | /// Returns the [ContextMenuButtonItem]s representing the buttons in this |
2041 | /// platform's default selection menu for an editable field. |
2042 | /// |
2043 | /// For example, [EditableText] uses this to generate the default buttons for |
2044 | /// its context menu. |
2045 | /// |
2046 | /// See also: |
2047 | /// |
2048 | /// * [EditableTextState.contextMenuButtonItems], which gives the |
2049 | /// [ContextMenuButtonItem]s for a specific EditableText. |
2050 | /// * [SelectableRegion.getSelectableButtonItems], which performs a similar |
2051 | /// role but for content that is selectable but not editable. |
2052 | /// * [AdaptiveTextSelectionToolbar], which builds the toolbar itself, and can |
2053 | /// take a list of [ContextMenuButtonItem]s with |
2054 | /// [AdaptiveTextSelectionToolbar.buttonItems]. |
2055 | /// * [AdaptiveTextSelectionToolbar.getAdaptiveButtons], which builds the button |
2056 | /// Widgets for the current platform given [ContextMenuButtonItem]s. |
2057 | static List<ContextMenuButtonItem> getEditableButtonItems({ |
2058 | required final ClipboardStatus? clipboardStatus, |
2059 | required final VoidCallback? onCopy, |
2060 | required final VoidCallback? onCut, |
2061 | required final VoidCallback? onPaste, |
2062 | required final VoidCallback? onSelectAll, |
2063 | required final VoidCallback? onLookUp, |
2064 | required final VoidCallback? onSearchWeb, |
2065 | required final VoidCallback? onShare, |
2066 | required final VoidCallback? onLiveTextInput, |
2067 | }) { |
2068 | final List<ContextMenuButtonItem> resultButtonItem = <ContextMenuButtonItem>[]; |
2069 | |
2070 | // Configure button items with clipboard. |
2071 | if (onPaste == null || clipboardStatus != ClipboardStatus.unknown) { |
2072 | // If the paste button is enabled, don't render anything until the state |
2073 | // of the clipboard is known, since it's used to determine if paste is |
2074 | // shown. |
2075 | |
2076 | // On Android, the share button is before the select all button. |
2077 | final bool showShareBeforeSelectAll = defaultTargetPlatform == TargetPlatform.android; |
2078 | |
2079 | resultButtonItem.addAll(<ContextMenuButtonItem>[ |
2080 | if (onCut != null) ContextMenuButtonItem(onPressed: onCut, type: ContextMenuButtonType.cut), |
2081 | if (onCopy != null) |
2082 | ContextMenuButtonItem(onPressed: onCopy, type: ContextMenuButtonType.copy), |
2083 | if (onPaste != null) |
2084 | ContextMenuButtonItem(onPressed: onPaste, type: ContextMenuButtonType.paste), |
2085 | if (onShare != null && showShareBeforeSelectAll) |
2086 | ContextMenuButtonItem(onPressed: onShare, type: ContextMenuButtonType.share), |
2087 | if (onSelectAll != null) |
2088 | ContextMenuButtonItem(onPressed: onSelectAll, type: ContextMenuButtonType.selectAll), |
2089 | if (onLookUp != null) |
2090 | ContextMenuButtonItem(onPressed: onLookUp, type: ContextMenuButtonType.lookUp), |
2091 | if (onSearchWeb != null) |
2092 | ContextMenuButtonItem(onPressed: onSearchWeb, type: ContextMenuButtonType.searchWeb), |
2093 | if (onShare != null && !showShareBeforeSelectAll) |
2094 | ContextMenuButtonItem(onPressed: onShare, type: ContextMenuButtonType.share), |
2095 | ]); |
2096 | } |
2097 | |
2098 | // Config button items with Live Text. |
2099 | if (onLiveTextInput != null) { |
2100 | resultButtonItem.add( |
2101 | ContextMenuButtonItem( |
2102 | onPressed: onLiveTextInput, |
2103 | type: ContextMenuButtonType.liveTextInput, |
2104 | ), |
2105 | ); |
2106 | } |
2107 | |
2108 | return resultButtonItem; |
2109 | } |
2110 | |
2111 | // Infer the keyboard type of an `EditableText` if it's not specified. |
2112 | static TextInputType _inferKeyboardType({ |
2113 | required Iterable<String>? autofillHints, |
2114 | required int? maxLines, |
2115 | }) { |
2116 | if (autofillHints == null || autofillHints.isEmpty) { |
2117 | return maxLines == 1 ? TextInputType.text : TextInputType.multiline; |
2118 | } |
2119 | |
2120 | final String effectiveHint = autofillHints.first; |
2121 | |
2122 | // On iOS oftentimes specifying a text content type is not enough to qualify |
2123 | // the input field for autofill. The keyboard type also needs to be compatible |
2124 | // with the content type. To get autofill to work by default on EditableText, |
2125 | // the keyboard type inference on iOS is done differently from other platforms. |
2126 | // |
2127 | // The entries with "autofill not working" comments are the iOS text content |
2128 | // types that should work with the specified keyboard type but won't trigger |
2129 | // (even within a native app). Tested on iOS 13.5. |
2130 | if (!kIsWeb) { |
2131 | switch (defaultTargetPlatform) { |
2132 | case TargetPlatform.iOS: |
2133 | case TargetPlatform.macOS: |
2134 | const Map<String, TextInputType> iOSKeyboardType = <String, TextInputType>{ |
2135 | AutofillHints.addressCity: TextInputType.name, |
2136 | AutofillHints.addressCityAndState: TextInputType.name, // Autofill not working. |
2137 | AutofillHints.addressState: TextInputType.name, |
2138 | AutofillHints.countryName: TextInputType.name, |
2139 | AutofillHints.creditCardNumber: TextInputType.number, // Couldn't test. |
2140 | AutofillHints.email: TextInputType.emailAddress, |
2141 | AutofillHints.familyName: TextInputType.name, |
2142 | AutofillHints.fullStreetAddress: TextInputType.name, |
2143 | AutofillHints.givenName: TextInputType.name, |
2144 | AutofillHints.jobTitle: TextInputType.name, // Autofill not working. |
2145 | AutofillHints.location: TextInputType.name, // Autofill not working. |
2146 | AutofillHints.middleName: TextInputType.name, // Autofill not working. |
2147 | AutofillHints.name: TextInputType.name, |
2148 | AutofillHints.namePrefix: TextInputType.name, // Autofill not working. |
2149 | AutofillHints.nameSuffix: TextInputType.name, // Autofill not working. |
2150 | AutofillHints.newPassword: TextInputType.text, |
2151 | AutofillHints.newUsername: TextInputType.text, |
2152 | AutofillHints.nickname: TextInputType.name, // Autofill not working. |
2153 | AutofillHints.oneTimeCode: TextInputType.number, |
2154 | AutofillHints.organizationName: TextInputType.text, // Autofill not working. |
2155 | AutofillHints.password: TextInputType.text, |
2156 | AutofillHints.postalCode: TextInputType.name, |
2157 | AutofillHints.streetAddressLine1: TextInputType.name, |
2158 | AutofillHints.streetAddressLine2: TextInputType.name, // Autofill not working. |
2159 | AutofillHints.sublocality: TextInputType.name, // Autofill not working. |
2160 | AutofillHints.telephoneNumber: TextInputType.name, |
2161 | AutofillHints.url: TextInputType.url, // Autofill not working. |
2162 | AutofillHints.username: TextInputType.text, |
2163 | }; |
2164 | |
2165 | final TextInputType? keyboardType = iOSKeyboardType[effectiveHint]; |
2166 | if (keyboardType != null) { |
2167 | return keyboardType; |
2168 | } |
2169 | case TargetPlatform.android: |
2170 | case TargetPlatform.fuchsia: |
2171 | case TargetPlatform.linux: |
2172 | case TargetPlatform.windows: |
2173 | break; |
2174 | } |
2175 | } |
2176 | |
2177 | if (maxLines != 1) { |
2178 | return TextInputType.multiline; |
2179 | } |
2180 | |
2181 | const Map<String, TextInputType> inferKeyboardType = <String, TextInputType>{ |
2182 | AutofillHints.addressCity: TextInputType.streetAddress, |
2183 | AutofillHints.addressCityAndState: TextInputType.streetAddress, |
2184 | AutofillHints.addressState: TextInputType.streetAddress, |
2185 | AutofillHints.birthday: TextInputType.datetime, |
2186 | AutofillHints.birthdayDay: TextInputType.datetime, |
2187 | AutofillHints.birthdayMonth: TextInputType.datetime, |
2188 | AutofillHints.birthdayYear: TextInputType.datetime, |
2189 | AutofillHints.countryCode: TextInputType.number, |
2190 | AutofillHints.countryName: TextInputType.text, |
2191 | AutofillHints.creditCardExpirationDate: TextInputType.datetime, |
2192 | AutofillHints.creditCardExpirationDay: TextInputType.datetime, |
2193 | AutofillHints.creditCardExpirationMonth: TextInputType.datetime, |
2194 | AutofillHints.creditCardExpirationYear: TextInputType.datetime, |
2195 | AutofillHints.creditCardFamilyName: TextInputType.name, |
2196 | AutofillHints.creditCardGivenName: TextInputType.name, |
2197 | AutofillHints.creditCardMiddleName: TextInputType.name, |
2198 | AutofillHints.creditCardName: TextInputType.name, |
2199 | AutofillHints.creditCardNumber: TextInputType.number, |
2200 | AutofillHints.creditCardSecurityCode: TextInputType.number, |
2201 | AutofillHints.creditCardType: TextInputType.text, |
2202 | AutofillHints.email: TextInputType.emailAddress, |
2203 | AutofillHints.familyName: TextInputType.name, |
2204 | AutofillHints.fullStreetAddress: TextInputType.streetAddress, |
2205 | AutofillHints.gender: TextInputType.text, |
2206 | AutofillHints.givenName: TextInputType.name, |
2207 | AutofillHints.impp: TextInputType.url, |
2208 | AutofillHints.jobTitle: TextInputType.text, |
2209 | AutofillHints.language: TextInputType.text, |
2210 | AutofillHints.location: TextInputType.streetAddress, |
2211 | AutofillHints.middleInitial: TextInputType.name, |
2212 | AutofillHints.middleName: TextInputType.name, |
2213 | AutofillHints.name: TextInputType.name, |
2214 | AutofillHints.namePrefix: TextInputType.name, |
2215 | AutofillHints.nameSuffix: TextInputType.name, |
2216 | AutofillHints.newPassword: TextInputType.text, |
2217 | AutofillHints.newUsername: TextInputType.text, |
2218 | AutofillHints.nickname: TextInputType.text, |
2219 | AutofillHints.oneTimeCode: TextInputType.text, |
2220 | AutofillHints.organizationName: TextInputType.text, |
2221 | AutofillHints.password: TextInputType.text, |
2222 | AutofillHints.photo: TextInputType.text, |
2223 | AutofillHints.postalAddress: TextInputType.streetAddress, |
2224 | AutofillHints.postalAddressExtended: TextInputType.streetAddress, |
2225 | AutofillHints.postalAddressExtendedPostalCode: TextInputType.number, |
2226 | AutofillHints.postalCode: TextInputType.number, |
2227 | AutofillHints.streetAddressLevel1: TextInputType.streetAddress, |
2228 | AutofillHints.streetAddressLevel2: TextInputType.streetAddress, |
2229 | AutofillHints.streetAddressLevel3: TextInputType.streetAddress, |
2230 | AutofillHints.streetAddressLevel4: TextInputType.streetAddress, |
2231 | AutofillHints.streetAddressLine1: TextInputType.streetAddress, |
2232 | AutofillHints.streetAddressLine2: TextInputType.streetAddress, |
2233 | AutofillHints.streetAddressLine3: TextInputType.streetAddress, |
2234 | AutofillHints.sublocality: TextInputType.streetAddress, |
2235 | AutofillHints.telephoneNumber: TextInputType.phone, |
2236 | AutofillHints.telephoneNumberAreaCode: TextInputType.phone, |
2237 | AutofillHints.telephoneNumberCountryCode: TextInputType.phone, |
2238 | AutofillHints.telephoneNumberDevice: TextInputType.phone, |
2239 | AutofillHints.telephoneNumberExtension: TextInputType.phone, |
2240 | AutofillHints.telephoneNumberLocal: TextInputType.phone, |
2241 | AutofillHints.telephoneNumberLocalPrefix: TextInputType.phone, |
2242 | AutofillHints.telephoneNumberLocalSuffix: TextInputType.phone, |
2243 | AutofillHints.telephoneNumberNational: TextInputType.phone, |
2244 | AutofillHints.transactionAmount: TextInputType.numberWithOptions(decimal: true), |
2245 | AutofillHints.transactionCurrency: TextInputType.text, |
2246 | AutofillHints.url: TextInputType.url, |
2247 | AutofillHints.username: TextInputType.text, |
2248 | }; |
2249 | |
2250 | return inferKeyboardType[effectiveHint] ?? TextInputType.text; |
2251 | } |
2252 | |
2253 | @override |
2254 | EditableTextState createState() => EditableTextState(); |
2255 | |
2256 | @override |
2257 | void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
2258 | super.debugFillProperties(properties); |
2259 | properties.add(DiagnosticsProperty<TextEditingController>('controller', controller)); |
2260 | properties.add(DiagnosticsProperty<FocusNode>('focusNode', focusNode)); |
2261 | properties.add(DiagnosticsProperty<bool>('obscureText', obscureText, defaultValue: false)); |
2262 | properties.add(DiagnosticsProperty<bool>('readOnly', readOnly, defaultValue: false)); |
2263 | properties.add(DiagnosticsProperty<bool>('autocorrect', autocorrect, defaultValue: true)); |
2264 | properties.add( |
2265 | EnumProperty<SmartDashesType>( |
2266 | 'smartDashesType', |
2267 | smartDashesType, |
2268 | defaultValue: obscureText ? SmartDashesType.disabled : SmartDashesType.enabled, |
2269 | ), |
2270 | ); |
2271 | properties.add( |
2272 | EnumProperty<SmartQuotesType>( |
2273 | 'smartQuotesType', |
2274 | smartQuotesType, |
2275 | defaultValue: obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled, |
2276 | ), |
2277 | ); |
2278 | properties.add( |
2279 | DiagnosticsProperty<bool>('enableSuggestions', enableSuggestions, defaultValue: true), |
2280 | ); |
2281 | style.debugFillProperties(properties); |
2282 | properties.add(EnumProperty<TextAlign>('textAlign', textAlign, defaultValue: null)); |
2283 | properties.add(EnumProperty<TextDirection>('textDirection', textDirection, defaultValue: null)); |
2284 | properties.add(DiagnosticsProperty<Locale>('locale', locale, defaultValue: null)); |
2285 | properties.add(DiagnosticsProperty<TextScaler>('textScaler', textScaler, defaultValue: null)); |
2286 | properties.add(IntProperty('maxLines', maxLines, defaultValue: 1)); |
2287 | properties.add(IntProperty('minLines', minLines, defaultValue: null)); |
2288 | properties.add(DiagnosticsProperty<bool>('expands', expands, defaultValue: false)); |
2289 | properties.add(DiagnosticsProperty<bool>('autofocus', autofocus, defaultValue: false)); |
2290 | properties.add( |
2291 | DiagnosticsProperty<TextInputType>('keyboardType', keyboardType, defaultValue: null), |
2292 | ); |
2293 | properties.add( |
2294 | DiagnosticsProperty<ScrollController>( |
2295 | 'scrollController', |
2296 | scrollController, |
2297 | defaultValue: null, |
2298 | ), |
2299 | ); |
2300 | properties.add( |
2301 | DiagnosticsProperty<ScrollPhysics>('scrollPhysics', scrollPhysics, defaultValue: null), |
2302 | ); |
2303 | properties.add( |
2304 | DiagnosticsProperty<Iterable<String>>('autofillHints', autofillHints, defaultValue: null), |
2305 | ); |
2306 | properties.add( |
2307 | DiagnosticsProperty<TextHeightBehavior>( |
2308 | 'textHeightBehavior', |
2309 | textHeightBehavior, |
2310 | defaultValue: null, |
2311 | ), |
2312 | ); |
2313 | properties.add( |
2314 | DiagnosticsProperty<bool>('scribbleEnabled', scribbleEnabled, defaultValue: true), |
2315 | ); |
2316 | properties.add( |
2317 | DiagnosticsProperty<bool>( |
2318 | 'stylusHandwritingEnabled', |
2319 | stylusHandwritingEnabled, |
2320 | defaultValue: defaultStylusHandwritingEnabled, |
2321 | ), |
2322 | ); |
2323 | properties.add( |
2324 | DiagnosticsProperty<bool>( |
2325 | 'enableIMEPersonalizedLearning', |
2326 | enableIMEPersonalizedLearning, |
2327 | defaultValue: true, |
2328 | ), |
2329 | ); |
2330 | properties.add( |
2331 | DiagnosticsProperty<bool>( |
2332 | 'enableInteractiveSelection', |
2333 | enableInteractiveSelection, |
2334 | defaultValue: true, |
2335 | ), |
2336 | ); |
2337 | properties.add( |
2338 | DiagnosticsProperty<UndoHistoryController>( |
2339 | 'undoController', |
2340 | undoController, |
2341 | defaultValue: null, |
2342 | ), |
2343 | ); |
2344 | properties.add( |
2345 | DiagnosticsProperty<SpellCheckConfiguration>( |
2346 | 'spellCheckConfiguration', |
2347 | spellCheckConfiguration, |
2348 | defaultValue: null, |
2349 | ), |
2350 | ); |
2351 | properties.add( |
2352 | DiagnosticsProperty<List<String>>( |
2353 | 'contentCommitMimeTypes', |
2354 | contentInsertionConfiguration?.allowedMimeTypes ?? const <String>[], |
2355 | defaultValue: |
2356 | contentInsertionConfiguration == null |
2357 | ? const <String>[] |
2358 | : kDefaultContentInsertionMimeTypes, |
2359 | ), |
2360 | ); |
2361 | } |
2362 | } |
2363 | |
2364 | /// State for an [EditableText]. |
2365 | class EditableTextState extends State<EditableText> |
2366 | with |
2367 | AutomaticKeepAliveClientMixin<EditableText>, |
2368 | WidgetsBindingObserver, |
2369 | TickerProviderStateMixin<EditableText>, |
2370 | TextSelectionDelegate, |
2371 | TextInputClient |
2372 | implements AutofillClient { |
2373 | Timer? _cursorTimer; |
2374 | AnimationController get _cursorBlinkOpacityController { |
2375 | return _backingCursorBlinkOpacityController ??= AnimationController(vsync: this) |
2376 | ..addListener(_onCursorColorTick); |
2377 | } |
2378 | |
2379 | AnimationController? _backingCursorBlinkOpacityController; |
2380 | late final Simulation _iosBlinkCursorSimulation = _DiscreteKeyFrameSimulation.iOSBlinkingCaret(); |
2381 | |
2382 | final ValueNotifier<bool> _cursorVisibilityNotifier = ValueNotifier<bool>(true); |
2383 | final GlobalKey _editableKey = GlobalKey(); |
2384 | |
2385 | /// Detects whether the clipboard can paste. |
2386 | final ClipboardStatusNotifier clipboardStatus = |
2387 | kIsWeb |
2388 | // Web browsers will show a permission dialog when Clipboard.hasStrings is |
2389 | // called. In an EditableText, this will happen before the paste button is |
2390 | // clicked, often before the context menu is even shown. To avoid this |
2391 | // poor user experience, always show the paste button on web. |
2392 | ? _WebClipboardStatusNotifier() |
2393 | : ClipboardStatusNotifier(); |
2394 | |
2395 | /// Detects whether the Live Text input is enabled. |
2396 | /// |
2397 | /// See also: |
2398 | /// * [LiveText], where the availability of Live Text input can be obtained. |
2399 | final LiveTextInputStatusNotifier? _liveTextInputStatus = |
2400 | kIsWeb ? null : LiveTextInputStatusNotifier(); |
2401 | |
2402 | TextInputConnection? _textInputConnection; |
2403 | bool get _hasInputConnection => _textInputConnection?.attached ?? false; |
2404 | |
2405 | TextSelectionOverlay? _selectionOverlay; |
2406 | ScrollNotificationObserverState? _scrollNotificationObserver; |
2407 | ({TextEditingValue value, Rect selectionBounds})? _dataWhenToolbarShowScheduled; |
2408 | bool _listeningToScrollNotificationObserver = false; |
2409 | |
2410 | bool get _webContextMenuEnabled => kIsWeb && BrowserContextMenu.enabled; |
2411 | |
2412 | final GlobalKey _scrollableKey = GlobalKey(); |
2413 | ScrollController? _internalScrollController; |
2414 | ScrollController get _scrollController => |
2415 | widget.scrollController ?? (_internalScrollController ??= ScrollController()); |
2416 | |
2417 | final LayerLink _toolbarLayerLink = LayerLink(); |
2418 | final LayerLink _startHandleLayerLink = LayerLink(); |
2419 | final LayerLink _endHandleLayerLink = LayerLink(); |
2420 | |
2421 | bool _didAutoFocus = false; |
2422 | |
2423 | AutofillGroupState? _currentAutofillScope; |
2424 | @override |
2425 | AutofillScope? get currentAutofillScope => _currentAutofillScope; |
2426 | |
2427 | AutofillClient get _effectiveAutofillClient => widget.autofillClient ?? this; |
2428 | |
2429 | late SpellCheckConfiguration _spellCheckConfiguration; |
2430 | late TextStyle _style; |
2431 | |
2432 | /// Configuration that determines how spell check will be performed. |
2433 | /// |
2434 | /// If possible, this configuration will contain a default for the |
2435 | /// [SpellCheckService] if it is not otherwise specified. |
2436 | /// |
2437 | /// See also: |
2438 | /// * [DefaultSpellCheckService], the spell check service used by default. |
2439 | @visibleForTesting |
2440 | SpellCheckConfiguration get spellCheckConfiguration => _spellCheckConfiguration; |
2441 | |
2442 | /// Whether or not spell check is enabled. |
2443 | /// |
2444 | /// Spell check is enabled when a [SpellCheckConfiguration] has been specified |
2445 | /// for the widget. |
2446 | bool get spellCheckEnabled => _spellCheckConfiguration.spellCheckEnabled; |
2447 | |
2448 | /// The most up-to-date spell check results for text input. |
2449 | /// |
2450 | /// These results will be updated via calls to spell check through a |
2451 | /// [SpellCheckService] and used by this widget to build the [TextSpan] tree |
2452 | /// for text input and menus for replacement suggestions of misspelled words. |
2453 | SpellCheckResults? spellCheckResults; |
2454 | |
2455 | bool get _spellCheckResultsReceived => |
2456 | spellCheckEnabled && |
2457 | spellCheckResults != null && |
2458 | spellCheckResults!.suggestionSpans.isNotEmpty; |
2459 | |
2460 | /// The text processing service used to retrieve the native text processing actions. |
2461 | final ProcessTextService _processTextService = DefaultProcessTextService(); |
2462 | |
2463 | /// The list of native text processing actions provided by the engine. |
2464 | final List<ProcessTextAction> _processTextActions = <ProcessTextAction>[]; |
2465 | |
2466 | /// Whether to create an input connection with the platform for text editing |
2467 | /// or not. |
2468 | /// |
2469 | /// Read-only input fields do not need a connection with the platform since |
2470 | /// there's no need for text editing capabilities (e.g. virtual keyboard). |
2471 | /// |
2472 | /// On macOS, most of the selection and focus related shortcuts require a |
2473 | /// connection with the platform because appropriate platform selectors are |
2474 | /// sent from the engine and translated into intents. For read-only fields |
2475 | /// those shortcuts should be available (for instance to allow tab traversal). |
2476 | /// |
2477 | /// On the web, we always need a connection because we want some browser |
2478 | /// functionalities to continue to work on read-only input fields like: |
2479 | /// - Relevant context menu. |
2480 | /// - cmd/ctrl+c shortcut to copy. |
2481 | /// - cmd/ctrl+a to select all. |
2482 | /// - Changing the selection using a physical keyboard. |
2483 | bool get _shouldCreateInputConnection => |
2484 | kIsWeb || defaultTargetPlatform == TargetPlatform.macOS || !widget.readOnly; |
2485 | |
2486 | // The time it takes for the floating cursor to snap to the text aligned |
2487 | // cursor position after the user has finished placing it. |
2488 | static const Duration _floatingCursorResetTime = Duration(milliseconds: 125); |
2489 | |
2490 | AnimationController? _floatingCursorResetController; |
2491 | |
2492 | Orientation? _lastOrientation; |
2493 | |
2494 | bool get _stylusHandwritingEnabled { |
2495 | // During the deprecation period, respect scribbleEnabled being explicitly |
2496 | // set. |
2497 | if (!widget.scribbleEnabled) { |
2498 | return widget.scribbleEnabled; |
2499 | } |
2500 | return widget.stylusHandwritingEnabled; |
2501 | } |
2502 | |
2503 | late final AppLifecycleListener _appLifecycleListener; |
2504 | bool _justResumed = false; |
2505 | |
2506 | @override |
2507 | bool get wantKeepAlive => widget.focusNode.hasFocus; |
2508 | |
2509 | Color get _cursorColor { |
2510 | final double effectiveOpacity = math.min( |
2511 | widget.cursorColor.alpha / 255.0, |
2512 | _cursorBlinkOpacityController.value, |
2513 | ); |
2514 | return widget.cursorColor.withOpacity(effectiveOpacity); |
2515 | } |
2516 | |
2517 | @override |
2518 | bool get cutEnabled { |
2519 | if (widget.selectionControls is! TextSelectionHandleControls) { |
2520 | return widget.toolbarOptions.cut && !widget.readOnly && !widget.obscureText; |
2521 | } |
2522 | return !widget.readOnly && !widget.obscureText && !textEditingValue.selection.isCollapsed; |
2523 | } |
2524 | |
2525 | @override |
2526 | bool get copyEnabled { |
2527 | if (widget.selectionControls is! TextSelectionHandleControls) { |
2528 | return widget.toolbarOptions.copy && !widget.obscureText; |
2529 | } |
2530 | return !widget.obscureText && !textEditingValue.selection.isCollapsed; |
2531 | } |
2532 | |
2533 | @override |
2534 | bool get pasteEnabled { |
2535 | if (widget.selectionControls is! TextSelectionHandleControls) { |
2536 | return widget.toolbarOptions.paste && !widget.readOnly; |
2537 | } |
2538 | return !widget.readOnly && (clipboardStatus.value == ClipboardStatus.pasteable); |
2539 | } |
2540 | |
2541 | @override |
2542 | bool get selectAllEnabled { |
2543 | if (widget.selectionControls is! TextSelectionHandleControls) { |
2544 | return widget.toolbarOptions.selectAll && |
2545 | (!widget.readOnly || !widget.obscureText) && |
2546 | widget.enableInteractiveSelection; |
2547 | } |
2548 | |
2549 | if (!widget.enableInteractiveSelection || (widget.readOnly && widget.obscureText)) { |
2550 | return false; |
2551 | } |
2552 | |
2553 | switch (defaultTargetPlatform) { |
2554 | case TargetPlatform.macOS: |
2555 | return false; |
2556 | case TargetPlatform.iOS: |
2557 | return textEditingValue.text.isNotEmpty && textEditingValue.selection.isCollapsed; |
2558 | case TargetPlatform.android: |
2559 | case TargetPlatform.fuchsia: |
2560 | case TargetPlatform.linux: |
2561 | case TargetPlatform.windows: |
2562 | return textEditingValue.text.isNotEmpty && |
2563 | !(textEditingValue.selection.start == 0 && |
2564 | textEditingValue.selection.end == textEditingValue.text.length); |
2565 | } |
2566 | } |
2567 | |
2568 | @override |
2569 | bool get lookUpEnabled { |
2570 | if (defaultTargetPlatform != TargetPlatform.iOS) { |
2571 | return false; |
2572 | } |
2573 | return !widget.obscureText && |
2574 | !textEditingValue.selection.isCollapsed && |
2575 | textEditingValue.selection.textInside(textEditingValue.text).trim() != ''; |
2576 | } |
2577 | |
2578 | @override |
2579 | bool get searchWebEnabled { |
2580 | if (defaultTargetPlatform != TargetPlatform.iOS) { |
2581 | return false; |
2582 | } |
2583 | |
2584 | return !widget.obscureText && |
2585 | !textEditingValue.selection.isCollapsed && |
2586 | textEditingValue.selection.textInside(textEditingValue.text).trim() != ''; |
2587 | } |
2588 | |
2589 | @override |
2590 | bool get shareEnabled { |
2591 | switch (defaultTargetPlatform) { |
2592 | case TargetPlatform.android: |
2593 | case TargetPlatform.iOS: |
2594 | return !widget.obscureText && |
2595 | !textEditingValue.selection.isCollapsed && |
2596 | textEditingValue.selection.textInside(textEditingValue.text).trim() != ''; |
2597 | case TargetPlatform.macOS: |
2598 | case TargetPlatform.fuchsia: |
2599 | case TargetPlatform.linux: |
2600 | case TargetPlatform.windows: |
2601 | return false; |
2602 | } |
2603 | } |
2604 | |
2605 | @override |
2606 | bool get liveTextInputEnabled { |
2607 | return _liveTextInputStatus?.value == LiveTextInputStatus.enabled && |
2608 | !widget.obscureText && |
2609 | !widget.readOnly && |
2610 | textEditingValue.selection.isCollapsed; |
2611 | } |
2612 | |
2613 | void _onChangedClipboardStatus() { |
2614 | setState(() { |
2615 | // Inform the widget that the value of clipboardStatus has changed. |
2616 | }); |
2617 | } |
2618 | |
2619 | void _onChangedLiveTextInputStatus() { |
2620 | setState(() { |
2621 | // Inform the widget that the value of liveTextInputStatus has changed. |
2622 | }); |
2623 | } |
2624 | |
2625 | TextEditingValue get _textEditingValueforTextLayoutMetrics { |
2626 | final Widget? editableWidget = _editableKey.currentContext?.widget; |
2627 | if (editableWidget is! _Editable) { |
2628 | throw StateError('_Editable must be mounted.'); |
2629 | } |
2630 | return editableWidget.value; |
2631 | } |
2632 | |
2633 | /// Copy current selection to [Clipboard]. |
2634 | @override |
2635 | void copySelection(SelectionChangedCause cause) { |
2636 | final TextSelection selection = textEditingValue.selection; |
2637 | if (selection.isCollapsed || widget.obscureText) { |
2638 | return; |
2639 | } |
2640 | final String text = textEditingValue.text; |
2641 | Clipboard.setData(ClipboardData(text: selection.textInside(text))); |
2642 | if (cause == SelectionChangedCause.toolbar) { |
2643 | bringIntoView(textEditingValue.selection.extent); |
2644 | hideToolbar(false); |
2645 | |
2646 | switch (defaultTargetPlatform) { |
2647 | case TargetPlatform.iOS: |
2648 | case TargetPlatform.macOS: |
2649 | case TargetPlatform.linux: |
2650 | case TargetPlatform.windows: |
2651 | break; |
2652 | case TargetPlatform.android: |
2653 | case TargetPlatform.fuchsia: |
2654 | // Collapse the selection and hide the toolbar and handles. |
2655 | userUpdateTextEditingValue( |
2656 | TextEditingValue( |
2657 | text: textEditingValue.text, |
2658 | selection: TextSelection.collapsed(offset: textEditingValue.selection.end), |
2659 | ), |
2660 | SelectionChangedCause.toolbar, |
2661 | ); |
2662 | } |
2663 | } |
2664 | clipboardStatus.update(); |
2665 | } |
2666 | |
2667 | /// Cut current selection to [Clipboard]. |
2668 | @override |
2669 | void cutSelection(SelectionChangedCause cause) { |
2670 | if (widget.readOnly || widget.obscureText) { |
2671 | return; |
2672 | } |
2673 | final TextSelection selection = textEditingValue.selection; |
2674 | final String text = textEditingValue.text; |
2675 | if (selection.isCollapsed) { |
2676 | return; |
2677 | } |
2678 | Clipboard.setData(ClipboardData(text: selection.textInside(text))); |
2679 | _replaceText(ReplaceTextIntent(textEditingValue, '', selection, cause)); |
2680 | if (cause == SelectionChangedCause.toolbar) { |
2681 | // Schedule a call to bringIntoView() after renderEditable updates. |
2682 | SchedulerBinding.instance.addPostFrameCallback((_) { |
2683 | if (mounted) { |
2684 | bringIntoView(textEditingValue.selection.extent); |
2685 | } |
2686 | }, debugLabel: 'EditableText.bringSelectionIntoView'); |
2687 | hideToolbar(); |
2688 | } |
2689 | clipboardStatus.update(); |
2690 | } |
2691 | |
2692 | bool get _allowPaste { |
2693 | return !widget.readOnly && textEditingValue.selection.isValid; |
2694 | } |
2695 | |
2696 | /// Paste text from [Clipboard]. |
2697 | @override |
2698 | Future<void> pasteText(SelectionChangedCause cause) async { |
2699 | if (!_allowPaste) { |
2700 | return; |
2701 | } |
2702 | // Snapshot the input before using `await`. |
2703 | // See https://github.com/flutter/flutter/issues/11427 |
2704 | final ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain); |
2705 | if (data == null) { |
2706 | return; |
2707 | } |
2708 | _pasteText(cause, data.text!); |
2709 | } |
2710 | |
2711 | void _pasteText(SelectionChangedCause cause, String text) { |
2712 | if (!_allowPaste) { |
2713 | return; |
2714 | } |
2715 | |
2716 | // After the paste, the cursor should be collapsed and located after the |
2717 | // pasted content. |
2718 | final TextSelection selection = textEditingValue.selection; |
2719 | final int lastSelectionIndex = math.max(selection.baseOffset, selection.extentOffset); |
2720 | final TextEditingValue collapsedTextEditingValue = textEditingValue.copyWith( |
2721 | selection: TextSelection.collapsed(offset: lastSelectionIndex), |
2722 | ); |
2723 | |
2724 | userUpdateTextEditingValue(collapsedTextEditingValue.replaced(selection, text), cause); |
2725 | if (cause == SelectionChangedCause.toolbar) { |
2726 | // Schedule a call to bringIntoView() after renderEditable updates. |
2727 | SchedulerBinding.instance.addPostFrameCallback((_) { |
2728 | if (mounted) { |
2729 | bringIntoView(textEditingValue.selection.extent); |
2730 | } |
2731 | }, debugLabel: 'EditableText.bringSelectionIntoView'); |
2732 | hideToolbar(); |
2733 | } |
2734 | } |
2735 | |
2736 | /// Select the entire text value. |
2737 | @override |
2738 | void selectAll(SelectionChangedCause cause) { |
2739 | if (widget.readOnly && widget.obscureText) { |
2740 | // If we can't modify it, and we can't copy it, there's no point in |
2741 | // selecting it. |
2742 | return; |
2743 | } |
2744 | userUpdateTextEditingValue( |
2745 | textEditingValue.copyWith( |
2746 | selection: TextSelection(baseOffset: 0, extentOffset: textEditingValue.text.length), |
2747 | ), |
2748 | cause, |
2749 | ); |
2750 | |
2751 | if (cause == SelectionChangedCause.toolbar) { |
2752 | switch (defaultTargetPlatform) { |
2753 | case TargetPlatform.android: |
2754 | case TargetPlatform.iOS: |
2755 | case TargetPlatform.fuchsia: |
2756 | break; |
2757 | case TargetPlatform.macOS: |
2758 | case TargetPlatform.linux: |
2759 | case TargetPlatform.windows: |
2760 | hideToolbar(); |
2761 | } |
2762 | switch (defaultTargetPlatform) { |
2763 | case TargetPlatform.android: |
2764 | case TargetPlatform.fuchsia: |
2765 | case TargetPlatform.linux: |
2766 | case TargetPlatform.windows: |
2767 | bringIntoView(textEditingValue.selection.extent); |
2768 | case TargetPlatform.macOS: |
2769 | case TargetPlatform.iOS: |
2770 | break; |
2771 | } |
2772 | } |
2773 | } |
2774 | |
2775 | /// Look up the current selection, |
2776 | /// as in the "Look Up" edit menu button on iOS. |
2777 | /// |
2778 | /// Currently this is only implemented for iOS. |
2779 | /// |
2780 | /// Throws an error if the selection is empty or collapsed. |
2781 | Future<void> lookUpSelection(SelectionChangedCause cause) async { |
2782 | assert(!widget.obscureText); |
2783 | |
2784 | final String text = textEditingValue.selection.textInside(textEditingValue.text); |
2785 | if (widget.obscureText || text.isEmpty) { |
2786 | return; |
2787 | } |
2788 | await SystemChannels.platform.invokeMethod('LookUp.invoke', text); |
2789 | } |
2790 | |
2791 | /// Launch a web search on the current selection, |
2792 | /// as in the "Search Web" edit menu button on iOS. |
2793 | /// |
2794 | /// Currently this is only implemented for iOS. |
2795 | /// |
2796 | /// When 'obscureText' is true or the selection is empty, |
2797 | /// this function will not do anything |
2798 | Future<void> searchWebForSelection(SelectionChangedCause cause) async { |
2799 | assert(!widget.obscureText); |
2800 | if (widget.obscureText) { |
2801 | return; |
2802 | } |
2803 | |
2804 | final String text = textEditingValue.selection.textInside(textEditingValue.text); |
2805 | if (text.isNotEmpty) { |
2806 | await SystemChannels.platform.invokeMethod('SearchWeb.invoke', text); |
2807 | } |
2808 | } |
2809 | |
2810 | /// Launch the share interface for the current selection, |
2811 | /// as in the "Share..." edit menu button on iOS. |
2812 | /// |
2813 | /// Currently this is only implemented for iOS and Android. |
2814 | /// |
2815 | /// When 'obscureText' is true or the selection is empty, |
2816 | /// this function will not do anything |
2817 | Future<void> shareSelection(SelectionChangedCause cause) async { |
2818 | assert(!widget.obscureText); |
2819 | if (widget.obscureText) { |
2820 | return; |
2821 | } |
2822 | |
2823 | final String text = textEditingValue.selection.textInside(textEditingValue.text); |
2824 | if (text.isNotEmpty) { |
2825 | await SystemChannels.platform.invokeMethod('Share.invoke', text); |
2826 | } |
2827 | } |
2828 | |
2829 | void _startLiveTextInput(SelectionChangedCause cause) { |
2830 | if (!liveTextInputEnabled) { |
2831 | return; |
2832 | } |
2833 | if (_hasInputConnection) { |
2834 | LiveText.startLiveTextInput(); |
2835 | } |
2836 | if (cause == SelectionChangedCause.toolbar) { |
2837 | hideToolbar(); |
2838 | } |
2839 | } |
2840 | |
2841 | /// Finds specified [SuggestionSpan] that matches the provided index using |
2842 | /// binary search. |
2843 | /// |
2844 | /// See also: |
2845 | /// |
2846 | /// * [SpellCheckSuggestionsToolbar], the Material style spell check |
2847 | /// suggestions toolbar that uses this method to render the correct |
2848 | /// suggestions in the toolbar for a misspelled word. |
2849 | SuggestionSpan? findSuggestionSpanAtCursorIndex(int cursorIndex) { |
2850 | if (!_spellCheckResultsReceived || |
2851 | spellCheckResults!.suggestionSpans.last.range.end < cursorIndex) { |
2852 | // No spell check results have been received or the cursor index is out |
2853 | // of range that suggestionSpans covers. |
2854 | return null; |
2855 | } |
2856 | |
2857 | final List<SuggestionSpan> suggestionSpans = spellCheckResults!.suggestionSpans; |
2858 | int leftIndex = 0; |
2859 | int rightIndex = suggestionSpans.length - 1; |
2860 | int midIndex = 0; |
2861 | |
2862 | while (leftIndex <= rightIndex) { |
2863 | midIndex = ((leftIndex + rightIndex) / 2).floor(); |
2864 | final int currentSpanStart = suggestionSpans[midIndex].range.start; |
2865 | final int currentSpanEnd = suggestionSpans[midIndex].range.end; |
2866 | |
2867 | if (cursorIndex <= currentSpanEnd && cursorIndex >= currentSpanStart) { |
2868 | return suggestionSpans[midIndex]; |
2869 | } else if (cursorIndex <= currentSpanStart) { |
2870 | rightIndex = midIndex - 1; |
2871 | } else { |
2872 | leftIndex = midIndex + 1; |
2873 | } |
2874 | } |
2875 | return null; |
2876 | } |
2877 | |
2878 | /// Infers the [SpellCheckConfiguration] used to perform spell check. |
2879 | /// |
2880 | /// If spell check is enabled, this will try to infer a value for |
2881 | /// the [SpellCheckService] if left unspecified. |
2882 | static SpellCheckConfiguration _inferSpellCheckConfiguration( |
2883 | SpellCheckConfiguration? configuration, |
2884 | ) { |
2885 | final SpellCheckService? spellCheckService = configuration?.spellCheckService; |
2886 | final bool spellCheckAutomaticallyDisabled = |
2887 | configuration == null || configuration == const SpellCheckConfiguration.disabled(); |
2888 | final bool spellCheckServiceIsConfigured = |
2889 | spellCheckService != null || |
2890 | WidgetsBinding.instance.platformDispatcher.nativeSpellCheckServiceDefined; |
2891 | if (spellCheckAutomaticallyDisabled || !spellCheckServiceIsConfigured) { |
2892 | // Only enable spell check if a non-disabled configuration is provided |
2893 | // and if that configuration does not specify a spell check service, |
2894 | // a native spell checker must be supported. |
2895 | assert(() { |
2896 | if (!spellCheckAutomaticallyDisabled && !spellCheckServiceIsConfigured) { |
2897 | FlutterError.reportError( |
2898 | FlutterErrorDetails( |
2899 | exception: FlutterError( |
2900 | 'Spell check was enabled with spellCheckConfiguration, but the ' |
2901 | 'current platform does not have a supported spell check ' |
2902 | 'service, and none was provided. Consider disabling spell ' |
2903 | 'check for this platform or passing a SpellCheckConfiguration ' |
2904 | 'with a specified spell check service.', |
2905 | ), |
2906 | library: 'widget library', |
2907 | stack: StackTrace.current, |
2908 | ), |
2909 | ); |
2910 | } |
2911 | return true; |
2912 | }()); |
2913 | return const SpellCheckConfiguration.disabled(); |
2914 | } |
2915 | |
2916 | return configuration.copyWith( |
2917 | spellCheckService: spellCheckService ?? DefaultSpellCheckService(), |
2918 | ); |
2919 | } |
2920 | |
2921 | /// Returns the [ContextMenuButtonItem]s for the given [ToolbarOptions]. |
2922 | @Deprecated( |
2923 | 'Use `contextMenuBuilder` instead of `toolbarOptions`. ' |
2924 | 'This feature was deprecated after v3.3.0-0.5.pre.', |
2925 | ) |
2926 | List<ContextMenuButtonItem>? buttonItemsForToolbarOptions([TargetPlatform? targetPlatform]) { |
2927 | final ToolbarOptions toolbarOptions = widget.toolbarOptions; |
2928 | if (toolbarOptions == ToolbarOptions.empty) { |
2929 | return null; |
2930 | } |
2931 | return <ContextMenuButtonItem>[ |
2932 | if (toolbarOptions.cut && cutEnabled) |
2933 | ContextMenuButtonItem( |
2934 | onPressed: () { |
2935 | cutSelection(SelectionChangedCause.toolbar); |
2936 | }, |
2937 | type: ContextMenuButtonType.cut, |
2938 | ), |
2939 | if (toolbarOptions.copy && copyEnabled) |
2940 | ContextMenuButtonItem( |
2941 | onPressed: () { |
2942 | copySelection(SelectionChangedCause.toolbar); |
2943 | }, |
2944 | type: ContextMenuButtonType.copy, |
2945 | ), |
2946 | if (toolbarOptions.paste && pasteEnabled) |
2947 | ContextMenuButtonItem( |
2948 | onPressed: () { |
2949 | pasteText(SelectionChangedCause.toolbar); |
2950 | }, |
2951 | type: ContextMenuButtonType.paste, |
2952 | ), |
2953 | if (toolbarOptions.selectAll && selectAllEnabled) |
2954 | ContextMenuButtonItem( |
2955 | onPressed: () { |
2956 | selectAll(SelectionChangedCause.toolbar); |
2957 | }, |
2958 | type: ContextMenuButtonType.selectAll, |
2959 | ), |
2960 | ]; |
2961 | } |
2962 | |
2963 | /// Gets the line heights at the start and end of the selection for the given |
2964 | /// [EditableTextState]. |
2965 | /// |
2966 | /// See also: |
2967 | /// |
2968 | /// * [TextSelectionToolbarAnchors.getSelectionRect], which depends on this |
2969 | /// information. |
2970 | ({double startGlyphHeight, double endGlyphHeight}) getGlyphHeights() { |
2971 | final TextSelection selection = textEditingValue.selection; |
2972 | |
2973 | // Only calculate handle rects if the text in the previous frame |
2974 | // is the same as the text in the current frame. This is done because |
2975 | // widget.renderObject contains the renderEditable from the previous frame. |
2976 | // If the text changed between the current and previous frames then |
2977 | // widget.renderObject.getRectForComposingRange might fail. In cases where |
2978 | // the current frame is different from the previous we fall back to |
2979 | // renderObject.preferredLineHeight. |
2980 | final InlineSpan span = renderEditable.text!; |
2981 | final String prevText = span.toPlainText(); |
2982 | final String currText = textEditingValue.text; |
2983 | if (prevText != currText || !selection.isValid || selection.isCollapsed) { |
2984 | return ( |
2985 | startGlyphHeight: renderEditable.preferredLineHeight, |
2986 | endGlyphHeight: renderEditable.preferredLineHeight, |
2987 | ); |
2988 | } |
2989 | |
2990 | final String selectedGraphemes = selection.textInside(currText); |
2991 | final int firstSelectedGraphemeExtent = selectedGraphemes.characters.first.length; |
2992 | final Rect? startCharacterRect = renderEditable.getRectForComposingRange( |
2993 | TextRange(start: selection.start, end: selection.start + firstSelectedGraphemeExtent), |
2994 | ); |
2995 | final int lastSelectedGraphemeExtent = selectedGraphemes.characters.last.length; |
2996 | final Rect? endCharacterRect = renderEditable.getRectForComposingRange( |
2997 | TextRange(start: selection.end - lastSelectedGraphemeExtent, end: selection.end), |
2998 | ); |
2999 | return ( |
3000 | startGlyphHeight: startCharacterRect?.height ?? renderEditable.preferredLineHeight, |
3001 | endGlyphHeight: endCharacterRect?.height ?? renderEditable.preferredLineHeight, |
3002 | ); |
3003 | } |
3004 | |
3005 | /// {@template flutter.widgets.EditableText.getAnchors} |
3006 | /// Returns the anchor points for the default context menu. |
3007 | /// {@endtemplate} |
3008 | /// |
3009 | /// See also: |
3010 | /// |
3011 | /// * [contextMenuButtonItems], which provides the [ContextMenuButtonItem]s |
3012 | /// for the default context menu buttons. |
3013 | TextSelectionToolbarAnchors get contextMenuAnchors { |
3014 | if (renderEditable.lastSecondaryTapDownPosition != null) { |
3015 | return TextSelectionToolbarAnchors( |
3016 | primaryAnchor: renderEditable.lastSecondaryTapDownPosition!, |
3017 | ); |
3018 | } |
3019 | |
3020 | final (startGlyphHeight: double startGlyphHeight, endGlyphHeight: double endGlyphHeight) = |
3021 | getGlyphHeights(); |
3022 | final TextSelection selection = textEditingValue.selection; |
3023 | final List<TextSelectionPoint> points = renderEditable.getEndpointsForSelection(selection); |
3024 | return TextSelectionToolbarAnchors.fromSelection( |
3025 | renderBox: renderEditable, |
3026 | startGlyphHeight: startGlyphHeight, |
3027 | endGlyphHeight: endGlyphHeight, |
3028 | selectionEndpoints: points, |
3029 | ); |
3030 | } |
3031 | |
3032 | /// Returns the [ContextMenuButtonItem]s representing the buttons in this |
3033 | /// platform's default selection menu for [EditableText]. |
3034 | /// |
3035 | /// See also: |
3036 | /// |
3037 | /// * [EditableText.getEditableButtonItems], which performs a similar role, |
3038 | /// but for any editable field, not just specifically EditableText. |
3039 | /// * [SystemContextMenu.getDefaultItems], which performs a similar role, but |
3040 | /// for the system-rendered context menu. |
3041 | /// * [SelectableRegionState.contextMenuButtonItems], which performs a similar |
3042 | /// role but for content that is selectable but not editable. |
3043 | /// * [contextMenuAnchors], which provides the anchor points for the default |
3044 | /// context menu. |
3045 | /// * [AdaptiveTextSelectionToolbar], which builds the toolbar itself, and can |
3046 | /// take a list of [ContextMenuButtonItem]s with |
3047 | /// [AdaptiveTextSelectionToolbar.buttonItems]. |
3048 | /// * [AdaptiveTextSelectionToolbar.getAdaptiveButtons], which builds the |
3049 | /// button Widgets for the current platform given [ContextMenuButtonItem]s. |
3050 | List<ContextMenuButtonItem> get contextMenuButtonItems { |
3051 | return buttonItemsForToolbarOptions() ?? |
3052 | EditableText.getEditableButtonItems( |
3053 | clipboardStatus: clipboardStatus.value, |
3054 | onCopy: copyEnabled ? () => copySelection(SelectionChangedCause.toolbar) : null, |
3055 | onCut: cutEnabled ? () => cutSelection(SelectionChangedCause.toolbar) : null, |
3056 | onPaste: pasteEnabled ? () => pasteText(SelectionChangedCause.toolbar) : null, |
3057 | onSelectAll: selectAllEnabled ? () => selectAll(SelectionChangedCause.toolbar) : null, |
3058 | onLookUp: lookUpEnabled ? () => lookUpSelection(SelectionChangedCause.toolbar) : null, |
3059 | onSearchWeb: |
3060 | searchWebEnabled |
3061 | ? () => searchWebForSelection(SelectionChangedCause.toolbar) |
3062 | : null, |
3063 | onShare: shareEnabled ? () => shareSelection(SelectionChangedCause.toolbar) : null, |
3064 | onLiveTextInput: |
3065 | liveTextInputEnabled |
3066 | ? () => _startLiveTextInput(SelectionChangedCause.toolbar) |
3067 | : null, |
3068 | ) |
3069 | ..addAll(_textProcessingActionButtonItems); |
3070 | } |
3071 | |
3072 | List<ContextMenuButtonItem> get _textProcessingActionButtonItems { |
3073 | final List<ContextMenuButtonItem> buttonItems = <ContextMenuButtonItem>[]; |
3074 | final TextSelection selection = textEditingValue.selection; |
3075 | if (widget.obscureText || !selection.isValid || selection.isCollapsed) { |
3076 | return buttonItems; |
3077 | } |
3078 | |
3079 | for (final ProcessTextAction action in _processTextActions) { |
3080 | buttonItems.add( |
3081 | ContextMenuButtonItem( |
3082 | label: action.label, |
3083 | onPressed: () async { |
3084 | final String selectedText = selection.textInside(textEditingValue.text); |
3085 | if (selectedText.isNotEmpty) { |
3086 | final String? processedText = await _processTextService.processTextAction( |
3087 | action.id, |
3088 | selectedText, |
3089 | widget.readOnly, |
3090 | ); |
3091 | // If an activity does not return a modified version, just hide the toolbar. |
3092 | // Otherwise use the result to replace the selected text. |
3093 | if (processedText != null && _allowPaste) { |
3094 | _pasteText(SelectionChangedCause.toolbar, processedText); |
3095 | } else { |
3096 | hideToolbar(); |
3097 | } |
3098 | } |
3099 | }, |
3100 | ), |
3101 | ); |
3102 | } |
3103 | return buttonItems; |
3104 | } |
3105 | |
3106 | // State lifecycle: |
3107 | |
3108 | @protected |
3109 | @override |
3110 | void initState() { |
3111 | super.initState(); |
3112 | _liveTextInputStatus?.addListener(_onChangedLiveTextInputStatus); |
3113 | clipboardStatus.addListener(_onChangedClipboardStatus); |
3114 | widget.controller.addListener(_didChangeTextEditingValue); |
3115 | widget.focusNode.addListener(_handleFocusChanged); |
3116 | _cursorVisibilityNotifier.value = widget.showCursor; |
3117 | _spellCheckConfiguration = _inferSpellCheckConfiguration(widget.spellCheckConfiguration); |
3118 | _appLifecycleListener = AppLifecycleListener(onResume: () => _justResumed = true); |
3119 | _initProcessTextActions(); |
3120 | } |
3121 | |
3122 | /// Query the engine to initialize the list of text processing actions to show |
3123 | /// in the text selection toolbar. |
3124 | Future<void> _initProcessTextActions() async { |
3125 | _processTextActions.clear(); |
3126 | _processTextActions.addAll(await _processTextService.queryTextActions()); |
3127 | } |
3128 | |
3129 | // Whether `TickerMode.of(context)` is true and animations (like blinking the |
3130 | // cursor) are supposed to run. |
3131 | bool _tickersEnabled = true; |
3132 | |
3133 | @protected |
3134 | @override |
3135 | void didChangeDependencies() { |
3136 | super.didChangeDependencies(); |
3137 | |
3138 | _style = |
3139 | MediaQuery.boldTextOf(context) |
3140 | ? widget.style.merge(const TextStyle(fontWeight: FontWeight.bold)) |
3141 | : widget.style; |
3142 | |
3143 | final AutofillGroupState? newAutofillGroup = AutofillGroup.maybeOf(context); |
3144 | if (currentAutofillScope != newAutofillGroup) { |
3145 | _currentAutofillScope?.unregister(autofillId); |
3146 | _currentAutofillScope = newAutofillGroup; |
3147 | _currentAutofillScope?.register(_effectiveAutofillClient); |
3148 | } |
3149 | |
3150 | if (!_didAutoFocus && widget.autofocus) { |
3151 | _didAutoFocus = true; |
3152 | SchedulerBinding.instance.addPostFrameCallback((_) { |
3153 | if (mounted && renderEditable.hasSize) { |
3154 | _flagInternalFocus(); |
3155 | FocusScope.of(context).autofocus(widget.focusNode); |
3156 | } |
3157 | }, debugLabel: 'EditableText.autofocus'); |
3158 | } |
3159 | |
3160 | // Restart or stop the blinking cursor when TickerMode changes. |
3161 | final bool newTickerEnabled = TickerMode.of(context); |
3162 | if (_tickersEnabled != newTickerEnabled) { |
3163 | _tickersEnabled = newTickerEnabled; |
3164 | if (_showBlinkingCursor) { |
3165 | _startCursorBlink(); |
3166 | } else if (!_tickersEnabled && _cursorTimer != null) { |
3167 | _stopCursorBlink(); |
3168 | } |
3169 | } |
3170 | |
3171 | // Check for changes in viewId. |
3172 | if (_hasInputConnection) { |
3173 | final int newViewId = View.of(context).viewId; |
3174 | if (newViewId != _viewId) { |
3175 | _textInputConnection!.updateConfig(_effectiveAutofillClient.textInputConfiguration); |
3176 | } |
3177 | } |
3178 | |
3179 | if (defaultTargetPlatform != TargetPlatform.iOS && |
3180 | defaultTargetPlatform != TargetPlatform.android) { |
3181 | return; |
3182 | } |
3183 | |
3184 | // Hide the text selection toolbar on mobile when orientation changes. |
3185 | final Orientation orientation = MediaQuery.orientationOf(context); |
3186 | if (_lastOrientation == null) { |
3187 | _lastOrientation = orientation; |
3188 | return; |
3189 | } |
3190 | if (orientation != _lastOrientation) { |
3191 | _lastOrientation = orientation; |
3192 | if (defaultTargetPlatform == TargetPlatform.iOS) { |
3193 | hideToolbar(false); |
3194 | } |
3195 | if (defaultTargetPlatform == TargetPlatform.android) { |
3196 | hideToolbar(); |
3197 | } |
3198 | } |
3199 | |
3200 | if (_listeningToScrollNotificationObserver) { |
3201 | // Only update subscription when we have previously subscribed to the |
3202 | // scroll notification observer. We only subscribe to the scroll |
3203 | // notification observer when the context menu is shown on platforms that |
3204 | // support _platformSupportsFadeOnScroll. |
3205 | _scrollNotificationObserver?.removeListener(_handleContextMenuOnParentScroll); |
3206 | _scrollNotificationObserver = ScrollNotificationObserver.maybeOf(context); |
3207 | _scrollNotificationObserver?.addListener(_handleContextMenuOnParentScroll); |
3208 | } |
3209 | } |
3210 | |
3211 | @protected |
3212 | @override |
3213 | void didUpdateWidget(EditableText oldWidget) { |
3214 | super.didUpdateWidget(oldWidget); |
3215 | if (widget.controller != oldWidget.controller) { |
3216 | oldWidget.controller.removeListener(_didChangeTextEditingValue); |
3217 | widget.controller.addListener(_didChangeTextEditingValue); |
3218 | _updateRemoteEditingValueIfNeeded(); |
3219 | } |
3220 | |
3221 | if (_selectionOverlay != null && |
3222 | (widget.contextMenuBuilder != oldWidget.contextMenuBuilder || |
3223 | widget.selectionControls != oldWidget.selectionControls || |
3224 | widget.onSelectionHandleTapped != oldWidget.onSelectionHandleTapped || |
3225 | widget.dragStartBehavior != oldWidget.dragStartBehavior || |
3226 | widget.magnifierConfiguration != oldWidget.magnifierConfiguration)) { |
3227 | final bool shouldShowToolbar = _selectionOverlay!.toolbarIsVisible; |
3228 | final bool shouldShowHandles = _selectionOverlay!.handlesVisible; |
3229 | _selectionOverlay!.dispose(); |
3230 | _selectionOverlay = _createSelectionOverlay(); |
3231 | if (shouldShowToolbar || shouldShowHandles) { |
3232 | SchedulerBinding.instance.addPostFrameCallback((Duration _) { |
3233 | if (shouldShowToolbar) { |
3234 | _selectionOverlay!.showToolbar(); |
3235 | } |
3236 | if (shouldShowHandles) { |
3237 | _selectionOverlay!.showHandles(); |
3238 | } |
3239 | }); |
3240 | } |
3241 | } else if (widget.controller.selection != oldWidget.controller.selection) { |
3242 | _selectionOverlay?.update(_value); |
3243 | } |
3244 | _selectionOverlay?.handlesVisible = widget.showSelectionHandles; |
3245 | |
3246 | if (widget.autofillClient != oldWidget.autofillClient) { |
3247 | _currentAutofillScope?.unregister(oldWidget.autofillClient?.autofillId ?? autofillId); |
3248 | _currentAutofillScope?.register(_effectiveAutofillClient); |
3249 | } |
3250 | |
3251 | if (widget.focusNode != oldWidget.focusNode) { |
3252 | oldWidget.focusNode.removeListener(_handleFocusChanged); |
3253 | widget.focusNode.addListener(_handleFocusChanged); |
3254 | updateKeepAlive(); |
3255 | } |
3256 | |
3257 | if (!_shouldCreateInputConnection) { |
3258 | _closeInputConnectionIfNeeded(); |
3259 | } else if (oldWidget.readOnly && _hasFocus) { |
3260 | // _openInputConnection must be called after layout information is available. |
3261 | // See https://github.com/flutter/flutter/issues/126312 |
3262 | SchedulerBinding.instance.addPostFrameCallback((Duration _) { |
3263 | _openInputConnection(); |
3264 | }, debugLabel: 'EditableText.openInputConnection'); |
3265 | } |
3266 | |
3267 | if (kIsWeb && _hasInputConnection) { |
3268 | if (oldWidget.readOnly != widget.readOnly) { |
3269 | _textInputConnection!.updateConfig(_effectiveAutofillClient.textInputConfiguration); |
3270 | } |
3271 | } |
3272 | |
3273 | if (_hasInputConnection) { |
3274 | if (oldWidget.obscureText != widget.obscureText || |
3275 | oldWidget.keyboardType != widget.keyboardType) { |
3276 | _textInputConnection!.updateConfig(_effectiveAutofillClient.textInputConfiguration); |
3277 | } |
3278 | } |
3279 | |
3280 | if (widget.style != oldWidget.style) { |
3281 | // The _textInputConnection will pick up the new style when it attaches in |
3282 | // _openInputConnection. |
3283 | _style = |
3284 | MediaQuery.boldTextOf(context) |
3285 | ? widget.style.merge(const TextStyle(fontWeight: FontWeight.bold)) |
3286 | : widget.style; |
3287 | if (_hasInputConnection) { |
3288 | _textInputConnection!.setStyle( |
3289 | fontFamily: _style.fontFamily, |
3290 | fontSize: _style.fontSize, |
3291 | fontWeight: _style.fontWeight, |
3292 | textDirection: _textDirection, |
3293 | textAlign: widget.textAlign, |
3294 | ); |
3295 | } |
3296 | } |
3297 | |
3298 | if (widget.showCursor != oldWidget.showCursor) { |
3299 | _startOrStopCursorTimerIfNeeded(); |
3300 | } |
3301 | final bool canPaste = |
3302 | widget.selectionControls is TextSelectionHandleControls |
3303 | ? pasteEnabled |
3304 | : widget.selectionControls?.canPaste(this) ?? false; |
3305 | if (widget.selectionEnabled && pasteEnabled && canPaste) { |
3306 | clipboardStatus.update(); |
3307 | } |
3308 | } |
3309 | |
3310 | void _disposeScrollNotificationObserver() { |
3311 | _listeningToScrollNotificationObserver = false; |
3312 | if (_scrollNotificationObserver != null) { |
3313 | _scrollNotificationObserver!.removeListener(_handleContextMenuOnParentScroll); |
3314 | _scrollNotificationObserver = null; |
3315 | } |
3316 | } |
3317 | |
3318 | @protected |
3319 | @override |
3320 | void dispose() { |
3321 | _internalScrollController?.dispose(); |
3322 | _currentAutofillScope?.unregister(autofillId); |
3323 | widget.controller.removeListener(_didChangeTextEditingValue); |
3324 | _floatingCursorResetController?.dispose(); |
3325 | _floatingCursorResetController = null; |
3326 | _closeInputConnectionIfNeeded(); |
3327 | assert(!_hasInputConnection); |
3328 | _cursorTimer?.cancel(); |
3329 | _cursorTimer = null; |
3330 | _backingCursorBlinkOpacityController?.dispose(); |
3331 | _backingCursorBlinkOpacityController = null; |
3332 | _selectionOverlay?.dispose(); |
3333 | _selectionOverlay = null; |
3334 | widget.focusNode.removeListener(_handleFocusChanged); |
3335 | WidgetsBinding.instance.removeObserver(this); |
3336 | _liveTextInputStatus?.removeListener(_onChangedLiveTextInputStatus); |
3337 | _liveTextInputStatus?.dispose(); |
3338 | clipboardStatus.removeListener(_onChangedClipboardStatus); |
3339 | clipboardStatus.dispose(); |
3340 | _cursorVisibilityNotifier.dispose(); |
3341 | _appLifecycleListener.dispose(); |
3342 | FocusManager.instance.removeListener(_unflagInternalFocus); |
3343 | _disposeScrollNotificationObserver(); |
3344 | super.dispose(); |
3345 | assert(_batchEditDepth <= 0, 'unfinished batch edits: $_batchEditDepth'); |
3346 | } |
3347 | |
3348 | // TextInputClient implementation: |
3349 | |
3350 | /// The last known [TextEditingValue] of the platform text input plugin. |
3351 | /// |
3352 | /// This value is updated when the platform text input plugin sends a new |
3353 | /// update via [updateEditingValue], or when [EditableText] calls |
3354 | /// [TextInputConnection.setEditingState] to overwrite the platform text input |
3355 | /// plugin's [TextEditingValue]. |
3356 | /// |
3357 | /// Used in [_updateRemoteEditingValueIfNeeded] to determine whether the |
3358 | /// remote value is outdated and needs updating. |
3359 | TextEditingValue? _lastKnownRemoteTextEditingValue; |
3360 | |
3361 | @override |
3362 | TextEditingValue get currentTextEditingValue => _value; |
3363 | |
3364 | @override |
3365 | void updateEditingValue(TextEditingValue value) { |
3366 | // This method handles text editing state updates from the platform text |
3367 | // input plugin. The [EditableText] may not have the focus or an open input |
3368 | // connection, as autofill can update a disconnected [EditableText]. |
3369 | |
3370 | // Since we still have to support keyboard select, this is the best place |
3371 | // to disable text updating. |
3372 | if (!_shouldCreateInputConnection) { |
3373 | return; |
3374 | } |
3375 | |
3376 | if (_checkNeedsAdjustAffinity(value)) { |
3377 | value = value.copyWith( |
3378 | selection: value.selection.copyWith(affinity: _value.selection.affinity), |
3379 | ); |
3380 | } |
3381 | |
3382 | if (widget.readOnly) { |
3383 | // In the read-only case, we only care about selection changes, and reject |
3384 | // everything else. |
3385 | value = _value.copyWith(selection: value.selection); |
3386 | } |
3387 | _lastKnownRemoteTextEditingValue = value; |
3388 | |
3389 | if (value == _value) { |
3390 | // This is possible, for example, when the numeric keyboard is input, |
3391 | // the engine will notify twice for the same value. |
3392 | // Track at https://github.com/flutter/flutter/issues/65811 |
3393 | return; |
3394 | } |
3395 | |
3396 | if (value.text == _value.text && value.composing == _value.composing) { |
3397 | // `selection` is the only change. |
3398 | SelectionChangedCause cause; |
3399 | if (_textInputConnection?.scribbleInProgress ?? false) { |
3400 | cause = SelectionChangedCause.stylusHandwriting; |
3401 | } else if (_pointOffsetOrigin != null) { |
3402 | // For floating cursor selection when force pressing the space bar. |
3403 | cause = SelectionChangedCause.forcePress; |
3404 | } else { |
3405 | cause = SelectionChangedCause.keyboard; |
3406 | } |
3407 | _handleSelectionChanged(value.selection, cause); |
3408 | } else { |
3409 | if (value.text != _value.text) { |
3410 | // Hide the toolbar if the text was changed, but only hide the toolbar |
3411 | // overlay; the selection handle's visibility will be handled |
3412 | // by `_handleSelectionChanged`. https://github.com/flutter/flutter/issues/108673 |
3413 | hideToolbar(false); |
3414 | } |
3415 | _currentPromptRectRange = null; |
3416 | |
3417 | final bool revealObscuredInput = |
3418 | _hasInputConnection && |
3419 | widget.obscureText && |
3420 | WidgetsBinding.instance.platformDispatcher.brieflyShowPassword && |
3421 | value.text.length == _value.text.length + 1; |
3422 | |
3423 | _obscureShowCharTicksPending = revealObscuredInput ? _kObscureShowLatestCharCursorTicks : 0; |
3424 | _obscureLatestCharIndex = revealObscuredInput ? _value.selection.baseOffset : null; |
3425 | _formatAndSetValue(value, SelectionChangedCause.keyboard); |
3426 | } |
3427 | |
3428 | if (_showBlinkingCursor && _cursorTimer != null) { |
3429 | // To keep the cursor from blinking while typing, restart the timer here. |
3430 | _stopCursorBlink(resetCharTicks: false); |
3431 | _startCursorBlink(); |
3432 | } |
3433 | |
3434 | // Wherever the value is changed by the user, schedule a showCaretOnScreen |
3435 | // to make sure the user can see the changes they just made. Programmatic |
3436 | // changes to `textEditingValue` do not trigger the behavior even if the |
3437 | // text field is focused. |
3438 | _scheduleShowCaretOnScreen(withAnimation: true); |
3439 | } |
3440 | |
3441 | bool _checkNeedsAdjustAffinity(TextEditingValue value) { |
3442 | // Trust the engine affinity if the text changes or selection changes. |
3443 | return value.text == _value.text && |
3444 | value.selection.isCollapsed == _value.selection.isCollapsed && |
3445 | value.selection.start == _value.selection.start && |
3446 | value.selection.affinity != _value.selection.affinity; |
3447 | } |
3448 | |
3449 | @override |
3450 | void performAction(TextInputAction action) { |
3451 | switch (action) { |
3452 | case TextInputAction.newline: |
3453 | // If this is a multiline EditableText, do nothing for a "newline" |
3454 | // action; The newline is already inserted. Otherwise, finalize |
3455 | // editing. |
3456 | if (!_isMultiline) { |
3457 | _finalizeEditing(action, shouldUnfocus: true); |
3458 | } |
3459 | case TextInputAction.done: |
3460 | case TextInputAction.go: |
3461 | case TextInputAction.next: |
3462 | case TextInputAction.previous: |
3463 | case TextInputAction.search: |
3464 | case TextInputAction.send: |
3465 | _finalizeEditing(action, shouldUnfocus: true); |
3466 | case TextInputAction.continueAction: |
3467 | case TextInputAction.emergencyCall: |
3468 | case TextInputAction.join: |
3469 | case TextInputAction.none: |
3470 | case TextInputAction.route: |
3471 | case TextInputAction.unspecified: |
3472 | // Finalize editing, but don't give up focus because this keyboard |
3473 | // action does not imply the user is done inputting information. |
3474 | _finalizeEditing(action, shouldUnfocus: false); |
3475 | } |
3476 | } |
3477 | |
3478 | @override |
3479 | void performPrivateCommand(String action, Map<String, dynamic> data) { |
3480 | widget.onAppPrivateCommand?.call(action, data); |
3481 | } |
3482 | |
3483 | @override |
3484 | void insertContent(KeyboardInsertedContent content) { |
3485 | assert( |
3486 | widget.contentInsertionConfiguration?.allowedMimeTypes.contains(content.mimeType) ?? false, |
3487 | ); |
3488 | widget.contentInsertionConfiguration?.onContentInserted.call(content); |
3489 | } |
3490 | |
3491 | // The original position of the caret on FloatingCursorDragState.start. |
3492 | Offset? _startCaretCenter; |
3493 | |
3494 | // The most recent text position as determined by the location of the floating |
3495 | // cursor. |
3496 | TextPosition? _lastTextPosition; |
3497 | |
3498 | // The offset of the floating cursor as determined from the start call. |
3499 | Offset? _pointOffsetOrigin; |
3500 | |
3501 | // The most recent position of the floating cursor. |
3502 | Offset? _lastBoundedOffset; |
3503 | |
3504 | // Because the center of the cursor is preferredLineHeight / 2 below the touch |
3505 | // origin, but the touch origin is used to determine which line the cursor is |
3506 | // on, we need this offset to correctly render and move the cursor. |
3507 | Offset get _floatingCursorOffset => Offset(0, renderEditable.preferredLineHeight / 2); |
3508 | |
3509 | @override |
3510 | void updateFloatingCursor(RawFloatingCursorPoint point) { |
3511 | _floatingCursorResetController ??= AnimationController(vsync: this) |
3512 | ..addListener(_onFloatingCursorResetTick); |
3513 | switch (point.state) { |
3514 | case FloatingCursorDragState.Start: |
3515 | if (_floatingCursorResetController!.isAnimating) { |
3516 | _floatingCursorResetController!.stop(); |
3517 | _onFloatingCursorResetTick(); |
3518 | } |
3519 | // Stop cursor blinking and making it visible. |
3520 | _stopCursorBlink(resetCharTicks: false); |
3521 | _cursorBlinkOpacityController.value = 1.0; |
3522 | // We want to send in points that are centered around a (0,0) origin, so |
3523 | // we cache the position. |
3524 | _pointOffsetOrigin = point.offset; |
3525 | |
3526 | final Offset startCaretCenter; |
3527 | final TextPosition currentTextPosition; |
3528 | final bool shouldResetOrigin; |
3529 | // Only non-null when starting a floating cursor via long press. |
3530 | if (point.startLocation != null) { |
3531 | shouldResetOrigin = false; |
3532 | (startCaretCenter, currentTextPosition) = point.startLocation!; |
3533 | } else { |
3534 | shouldResetOrigin = true; |
3535 | currentTextPosition = TextPosition( |
3536 | offset: renderEditable.selection!.baseOffset, |
3537 | affinity: renderEditable.selection!.affinity, |
3538 | ); |
3539 | startCaretCenter = renderEditable.getLocalRectForCaret(currentTextPosition).center; |
3540 | } |
3541 | |
3542 | _startCaretCenter = startCaretCenter; |
3543 | _lastBoundedOffset = renderEditable.calculateBoundedFloatingCursorOffset( |
3544 | _startCaretCenter! - _floatingCursorOffset, |
3545 | shouldResetOrigin: shouldResetOrigin, |
3546 | ); |
3547 | _lastTextPosition = currentTextPosition; |
3548 | renderEditable.setFloatingCursor(point.state, _lastBoundedOffset!, _lastTextPosition!); |
3549 | case FloatingCursorDragState.Update: |
3550 | final Offset centeredPoint = point.offset! - _pointOffsetOrigin!; |
3551 | final Offset rawCursorOffset = _startCaretCenter! + centeredPoint - _floatingCursorOffset; |
3552 | |
3553 | _lastBoundedOffset = renderEditable.calculateBoundedFloatingCursorOffset(rawCursorOffset); |
3554 | _lastTextPosition = renderEditable.getPositionForPoint( |
3555 | renderEditable.localToGlobal(_lastBoundedOffset! + _floatingCursorOffset), |
3556 | ); |
3557 | renderEditable.setFloatingCursor(point.state, _lastBoundedOffset!, _lastTextPosition!); |
3558 | case FloatingCursorDragState.End: |
3559 | // Resume cursor blinking. |
3560 | _startCursorBlink(); |
3561 | // We skip animation if no update has happened. |
3562 | if (_lastTextPosition != null && _lastBoundedOffset != null) { |
3563 | _floatingCursorResetController!.value = 0.0; |
3564 | _floatingCursorResetController!.animateTo( |
3565 | 1.0, |
3566 | duration: _floatingCursorResetTime, |
3567 | curve: Curves.decelerate, |
3568 | ); |
3569 | } |
3570 | } |
3571 | } |
3572 | |
3573 | void _onFloatingCursorResetTick() { |
3574 | final Offset finalPosition = |
3575 | renderEditable.getLocalRectForCaret(_lastTextPosition!).centerLeft - _floatingCursorOffset; |
3576 | if (_floatingCursorResetController!.isCompleted) { |
3577 | renderEditable.setFloatingCursor( |
3578 | FloatingCursorDragState.End, |
3579 | finalPosition, |
3580 | _lastTextPosition!, |
3581 | ); |
3582 | // During a floating cursor's move gesture (1 finger), a cursor is |
3583 | // animated only visually, without actually updating the selection. |
3584 | // Only after move gesture is complete, this function will be called |
3585 | // to actually update the selection to the new cursor location with |
3586 | // zero selection length. |
3587 | |
3588 | // However, During a floating cursor's selection gesture (2 fingers), the |
3589 | // selection is constantly updated by the engine throughout the gesture. |
3590 | // Thus when the gesture is complete, we should not update the selection |
3591 | // to the cursor location with zero selection length, because that would |
3592 | // overwrite the selection made by floating cursor selection. |
3593 | |
3594 | // Here we use `isCollapsed` to distinguish between floating cursor's |
3595 | // move gesture (1 finger) vs selection gesture (2 fingers), as |
3596 | // the engine does not provide information other than notifying a |
3597 | // new selection during with selection gesture (2 fingers). |
3598 | if (renderEditable.selection!.isCollapsed) { |
3599 | // The cause is technically the force cursor, but the cause is listed as tap as the desired functionality is the same. |
3600 | _handleSelectionChanged( |
3601 | TextSelection.fromPosition(_lastTextPosition!), |
3602 | SelectionChangedCause.forcePress, |
3603 | ); |
3604 | } |
3605 | _startCaretCenter = null; |
3606 | _lastTextPosition = null; |
3607 | _pointOffsetOrigin = null; |
3608 | _lastBoundedOffset = null; |
3609 | } else { |
3610 | final double lerpValue = _floatingCursorResetController!.value; |
3611 | final double lerpX = ui.lerpDouble(_lastBoundedOffset!.dx, finalPosition.dx, lerpValue)!; |
3612 | final double lerpY = ui.lerpDouble(_lastBoundedOffset!.dy, finalPosition.dy, lerpValue)!; |
3613 | |
3614 | renderEditable.setFloatingCursor( |
3615 | FloatingCursorDragState.Update, |
3616 | Offset(lerpX, lerpY), |
3617 | _lastTextPosition!, |
3618 | resetLerpValue: lerpValue, |
3619 | ); |
3620 | } |
3621 | } |
3622 | |
3623 | @pragma('vm:notify-debugger-on-exception') |
3624 | void _finalizeEditing(TextInputAction action, {required bool shouldUnfocus}) { |
3625 | // Take any actions necessary now that the user has completed editing. |
3626 | if (widget.onEditingComplete != null) { |
3627 | try { |
3628 | widget.onEditingComplete!(); |
3629 | } catch (exception, stack) { |
3630 | FlutterError.reportError( |
3631 | FlutterErrorDetails( |
3632 | exception: exception, |
3633 | stack: stack, |
3634 | library: 'widgets', |
3635 | context: ErrorDescription('while calling onEditingComplete for $action'), |
3636 | ), |
3637 | ); |
3638 | } |
3639 | } else { |
3640 | // Default behavior if the developer did not provide an |
3641 | // onEditingComplete callback: Finalize editing and remove focus, or move |
3642 | // it to the next/previous field, depending on the action. |
3643 | widget.controller.clearComposing(); |
3644 | if (shouldUnfocus) { |
3645 | switch (action) { |
3646 | case TextInputAction.none: |
3647 | case TextInputAction.unspecified: |
3648 | case TextInputAction.done: |
3649 | case TextInputAction.go: |
3650 | case TextInputAction.search: |
3651 | case TextInputAction.send: |
3652 | case TextInputAction.continueAction: |
3653 | case TextInputAction.join: |
3654 | case TextInputAction.route: |
3655 | case TextInputAction.emergencyCall: |
3656 | case TextInputAction.newline: |
3657 | widget.focusNode.unfocus(); |
3658 | case TextInputAction.next: |
3659 | widget.focusNode.nextFocus(); |
3660 | case TextInputAction.previous: |
3661 | widget.focusNode.previousFocus(); |
3662 | } |
3663 | } |
3664 | } |
3665 | |
3666 | final ValueChanged<String>? onSubmitted = widget.onSubmitted; |
3667 | if (onSubmitted == null) { |
3668 | return; |
3669 | } |
3670 | |
3671 | // Invoke optional callback with the user's submitted content. |
3672 | try { |
3673 | onSubmitted(_value.text); |
3674 | } catch (exception, stack) { |
3675 | FlutterError.reportError( |
3676 | FlutterErrorDetails( |
3677 | exception: exception, |
3678 | stack: stack, |
3679 | library: 'widgets', |
3680 | context: ErrorDescription('while calling onSubmitted for $action'), |
3681 | ), |
3682 | ); |
3683 | } |
3684 | |
3685 | // If `shouldUnfocus` is true, the text field should no longer be focused |
3686 | // after the microtask queue is drained. But in case the developer cancelled |
3687 | // the focus change in the `onSubmitted` callback by focusing this input |
3688 | // field again, reset the soft keyboard. |
3689 | // See https://github.com/flutter/flutter/issues/84240. |
3690 | // |
3691 | // `_restartConnectionIfNeeded` creates a new TextInputConnection to replace |
3692 | // the current one. This on iOS switches to a new input view and on Android |
3693 | // restarts the input method, and in both cases the soft keyboard will be |
3694 | // reset. |
3695 | if (shouldUnfocus) { |
3696 | _scheduleRestartConnection(); |
3697 | } |
3698 | } |
3699 | |
3700 | int _batchEditDepth = 0; |
3701 | |
3702 | /// Begins a new batch edit, within which new updates made to the text editing |
3703 | /// value will not be sent to the platform text input plugin. |
3704 | /// |
3705 | /// Batch edits nest. When the outermost batch edit finishes, [endBatchEdit] |
3706 | /// will attempt to send [currentTextEditingValue] to the text input plugin if |
3707 | /// it detected a change. |
3708 | void beginBatchEdit() { |
3709 | _batchEditDepth += 1; |
3710 | } |
3711 | |
3712 | /// Ends the current batch edit started by the last call to [beginBatchEdit], |
3713 | /// and send [currentTextEditingValue] to the text input plugin if needed. |
3714 | /// |
3715 | /// Throws an error in debug mode if this [EditableText] is not in a batch |
3716 | /// edit. |
3717 | void endBatchEdit() { |
3718 | _batchEditDepth -= 1; |
3719 | assert( |
3720 | _batchEditDepth >= 0, |
3721 | 'Unbalanced call to endBatchEdit: beginBatchEdit must be called first.', |
3722 | ); |
3723 | _updateRemoteEditingValueIfNeeded(); |
3724 | } |
3725 | |
3726 | void _updateRemoteEditingValueIfNeeded() { |
3727 | if (_batchEditDepth > 0 || !_hasInputConnection) { |
3728 | return; |
3729 | } |
3730 | final TextEditingValue localValue = _value; |
3731 | if (localValue == _lastKnownRemoteTextEditingValue) { |
3732 | return; |
3733 | } |
3734 | _textInputConnection!.setEditingState(localValue); |
3735 | _lastKnownRemoteTextEditingValue = localValue; |
3736 | } |
3737 | |
3738 | TextEditingValue get _value => widget.controller.value; |
3739 | set _value(TextEditingValue value) { |
3740 | widget.controller.value = value; |
3741 | } |
3742 | |
3743 | bool get _hasFocus => widget.focusNode.hasFocus; |
3744 | bool get _isMultiline => widget.maxLines != 1; |
3745 | |
3746 | /// Flag to track whether this [EditableText] was in focus when [onTapOutside] |
3747 | /// was called. |
3748 | /// |
3749 | /// This is used to determine whether [onTapUpOutside] should be called. |
3750 | /// The reason [_hasFocus] can't be used directly is because [onTapOutside] |
3751 | /// might unfocus this [EditableText] and block the [onTapUpOutside] call. |
3752 | bool _hadFocusOnTapDown = false; |
3753 | |
3754 | // Finds the closest scroll offset to the current scroll offset that fully |
3755 | // reveals the given caret rect. If the given rect's main axis extent is too |
3756 | // large to be fully revealed in `renderEditable`, it will be centered along |
3757 | // the main axis. |
3758 | // |
3759 | // If this is a multiline EditableText (which means the Editable can only |
3760 | // scroll vertically), the given rect's height will first be extended to match |
3761 | // `renderEditable.preferredLineHeight`, before the target scroll offset is |
3762 | // calculated. |
3763 | RevealedOffset _getOffsetToRevealCaret(Rect rect) { |
3764 | if (!_scrollController.position.allowImplicitScrolling) { |
3765 | return RevealedOffset(offset: _scrollController.offset, rect: rect); |
3766 | } |
3767 | |
3768 | final Size editableSize = renderEditable.size; |
3769 | final double additionalOffset; |
3770 | final Offset unitOffset; |
3771 | |
3772 | if (!_isMultiline) { |
3773 | additionalOffset = |
3774 | rect.width >= editableSize.width |
3775 | // Center `rect` if it's oversized. |
3776 | ? editableSize.width / 2 - rect.center.dx |
3777 | // Valid additional offsets range from (rect.right - size.width) |
3778 | // to (rect.left). Pick the closest one if out of range. |
3779 | : clampDouble(0.0, rect.right - editableSize.width, rect.left); |
3780 | unitOffset = const Offset(1, 0); |
3781 | } else { |
3782 | // The caret is vertically centered within the line. Expand the caret's |
3783 | // height so that it spans the line because we're going to ensure that the |
3784 | // entire expanded caret is scrolled into view. |
3785 | final Rect expandedRect = Rect.fromCenter( |
3786 | center: rect.center, |
3787 | width: rect.width, |
3788 | height: math.max(rect.height, renderEditable.preferredLineHeight), |
3789 | ); |
3790 | |
3791 | additionalOffset = |
3792 | expandedRect.height >= editableSize.height |
3793 | ? editableSize.height / 2 - expandedRect.center.dy |
3794 | : clampDouble(0.0, expandedRect.bottom - editableSize.height, expandedRect.top); |
3795 | unitOffset = const Offset(0, 1); |
3796 | } |
3797 | |
3798 | // No overscrolling when encountering tall fonts/scripts that extend past |
3799 | // the ascent. |
3800 | final double targetOffset = clampDouble( |
3801 | additionalOffset + _scrollController.offset, |
3802 | _scrollController.position.minScrollExtent, |
3803 | _scrollController.position.maxScrollExtent, |
3804 | ); |
3805 | |
3806 | final double offsetDelta = _scrollController.offset - targetOffset; |
3807 | return RevealedOffset(rect: rect.shift(unitOffset * offsetDelta), offset: targetOffset); |
3808 | } |
3809 | |
3810 | /// Whether to send the autofill information to the autofill service. True by |
3811 | /// default. |
3812 | bool get _needsAutofill => |
3813 | _effectiveAutofillClient.textInputConfiguration.autofillConfiguration.enabled; |
3814 | |
3815 | // Must be called after layout. |
3816 | // See https://github.com/flutter/flutter/issues/126312 |
3817 | void _openInputConnection() { |
3818 | if (!_shouldCreateInputConnection) { |
3819 | return; |
3820 | } |
3821 | if (!_hasInputConnection) { |
3822 | final TextEditingValue localValue = _value; |
3823 | |
3824 | // When _needsAutofill == true && currentAutofillScope == null, autofill |
3825 | // is allowed but saving the user input from the text field is |
3826 | // discouraged. |
3827 | // |
3828 | // In case the autofillScope changes from a non-null value to null, or |
3829 | // _needsAutofill changes to false from true, the platform needs to be |
3830 | // notified to exclude this field from the autofill context. So we need to |
3831 | // provide the autofillId. |
3832 | _textInputConnection = |
3833 | _needsAutofill && currentAutofillScope != null |
3834 | ? currentAutofillScope!.attach(this, _effectiveAutofillClient.textInputConfiguration) |
3835 | : TextInput.attach(this, _effectiveAutofillClient.textInputConfiguration); |
3836 | _updateSizeAndTransform(); |
3837 | _schedulePeriodicPostFrameCallbacks(); |
3838 | _textInputConnection! |
3839 | ..setStyle( |
3840 | fontFamily: _style.fontFamily, |
3841 | fontSize: _style.fontSize, |
3842 | fontWeight: _style.fontWeight, |
3843 | textDirection: _textDirection, |
3844 | textAlign: widget.textAlign, |
3845 | ) |
3846 | ..setEditingState(localValue) |
3847 | ..show(); |
3848 | if (_needsAutofill) { |
3849 | // Request autofill AFTER the size and the transform have been sent to |
3850 | // the platform text input plugin. |
3851 | _textInputConnection!.requestAutofill(); |
3852 | } |
3853 | _lastKnownRemoteTextEditingValue = localValue; |
3854 | } else { |
3855 | _textInputConnection!.show(); |
3856 | } |
3857 | } |
3858 | |
3859 | void _closeInputConnectionIfNeeded() { |
3860 | if (_hasInputConnection) { |
3861 | _textInputConnection!.close(); |
3862 | _textInputConnection = null; |
3863 | _lastKnownRemoteTextEditingValue = null; |
3864 | _scribbleCacheKey = null; |
3865 | removeTextPlaceholder(); |
3866 | } |
3867 | } |
3868 | |
3869 | void _openOrCloseInputConnectionIfNeeded() { |
3870 | if (_hasFocus && widget.focusNode.consumeKeyboardToken()) { |
3871 | _openInputConnection(); |
3872 | } else if (!_hasFocus) { |
3873 | _closeInputConnectionIfNeeded(); |
3874 | widget.controller.clearComposing(); |
3875 | } |
3876 | } |
3877 | |
3878 | bool _restartConnectionScheduled = false; |
3879 | void _scheduleRestartConnection() { |
3880 | if (_restartConnectionScheduled) { |
3881 | return; |
3882 | } |
3883 | _restartConnectionScheduled = true; |
3884 | scheduleMicrotask(_restartConnectionIfNeeded); |
3885 | } |
3886 | |
3887 | // Discards the current [TextInputConnection] and establishes a new one. |
3888 | // |
3889 | // This method is rarely needed. This is currently used to reset the input |
3890 | // type when the "submit" text input action is triggered and the developer |
3891 | // puts the focus back to this input field.. |
3892 | void _restartConnectionIfNeeded() { |
3893 | _restartConnectionScheduled = false; |
3894 | if (!_hasInputConnection || !_shouldCreateInputConnection) { |
3895 | return; |
3896 | } |
3897 | _textInputConnection!.close(); |
3898 | _textInputConnection = null; |
3899 | _lastKnownRemoteTextEditingValue = null; |
3900 | |
3901 | final AutofillScope? currentAutofillScope = _needsAutofill ? this.currentAutofillScope : null; |
3902 | final TextInputConnection newConnection = |
3903 | currentAutofillScope?.attach(this, textInputConfiguration) ?? |
3904 | TextInput.attach(this, _effectiveAutofillClient.textInputConfiguration); |
3905 | _textInputConnection = newConnection; |
3906 | |
3907 | newConnection |
3908 | ..show() |
3909 | ..setStyle( |
3910 | fontFamily: _style.fontFamily, |
3911 | fontSize: _style.fontSize, |
3912 | fontWeight: _style.fontWeight, |
3913 | textDirection: _textDirection, |
3914 | textAlign: widget.textAlign, |
3915 | ) |
3916 | ..setEditingState(_value); |
3917 | _lastKnownRemoteTextEditingValue = _value; |
3918 | } |
3919 | |
3920 | @override |
3921 | void didChangeInputControl(TextInputControl? oldControl, TextInputControl? newControl) { |
3922 | if (_hasFocus && _hasInputConnection) { |
3923 | oldControl?.hide(); |
3924 | newControl?.show(); |
3925 | } |
3926 | } |
3927 | |
3928 | @override |
3929 | void connectionClosed() { |
3930 | if (_hasInputConnection) { |
3931 | _textInputConnection!.connectionClosedReceived(); |
3932 | _textInputConnection = null; |
3933 | _lastKnownRemoteTextEditingValue = null; |
3934 | widget.focusNode.unfocus(); |
3935 | } |
3936 | } |
3937 | |
3938 | // Indicates that a call to _handleFocusChanged originated within |
3939 | // EditableText, allowing it to distinguish between internal and external |
3940 | // focus changes. |
3941 | bool _nextFocusChangeIsInternal = false; |
3942 | |
3943 | // Sets _nextFocusChangeIsInternal to true only until any subsequent focus |
3944 | // change happens. |
3945 | void _flagInternalFocus() { |
3946 | _nextFocusChangeIsInternal = true; |
3947 | FocusManager.instance.addListener(_unflagInternalFocus); |
3948 | } |
3949 | |
3950 | void _unflagInternalFocus() { |
3951 | _nextFocusChangeIsInternal = false; |
3952 | FocusManager.instance.removeListener(_unflagInternalFocus); |
3953 | } |
3954 | |
3955 | /// Express interest in interacting with the keyboard. |
3956 | /// |
3957 | /// If this control is already attached to the keyboard, this function will |
3958 | /// request that the keyboard become visible. Otherwise, this function will |
3959 | /// ask the focus system that it become focused. If successful in acquiring |
3960 | /// focus, the control will then attach to the keyboard and request that the |
3961 | /// keyboard become visible. |
3962 | void requestKeyboard() { |
3963 | if (_hasFocus) { |
3964 | _openInputConnection(); |
3965 | } else { |
3966 | _flagInternalFocus(); |
3967 | widget.focusNode |
3968 | .requestFocus(); // This eventually calls _openInputConnection also, see _handleFocusChanged. |
3969 | } |
3970 | } |
3971 | |
3972 | void _updateOrDisposeSelectionOverlayIfNeeded() { |
3973 | if (_selectionOverlay != null) { |
3974 | if (_hasFocus) { |
3975 | _selectionOverlay!.update(_value); |
3976 | } else { |
3977 | _selectionOverlay!.dispose(); |
3978 | _selectionOverlay = null; |
3979 | } |
3980 | } |
3981 | } |
3982 | |
3983 | final bool _platformSupportsFadeOnScroll = switch (defaultTargetPlatform) { |
3984 | TargetPlatform.android || TargetPlatform.iOS => true, |
3985 | TargetPlatform.fuchsia || |
3986 | TargetPlatform.linux || |
3987 | TargetPlatform.macOS || |
3988 | TargetPlatform.windows => false, |
3989 | }; |
3990 | |
3991 | bool _isInternalScrollableNotification(BuildContext? notificationContext) { |
3992 | final ScrollableState? scrollableState = |
3993 | notificationContext?.findAncestorStateOfType<ScrollableState>(); |
3994 | return _scrollableKey.currentContext == scrollableState?.context; |
3995 | } |
3996 | |
3997 | bool _scrollableNotificationIsFromSameSubtree(BuildContext? notificationContext) { |
3998 | if (notificationContext == null) { |
3999 | return false; |
4000 | } |
4001 | BuildContext? currentContext = context; |
4002 | // The notification context of a ScrollNotification points to the RawGestureDetector |
4003 | // of the Scrollable. We get the ScrollableState associated with this notification |
4004 | // by looking up the tree. |
4005 | final ScrollableState? notificationScrollableState = |
4006 | notificationContext.findAncestorStateOfType<ScrollableState>(); |
4007 | if (notificationScrollableState == null) { |
4008 | return false; |
4009 | } |
4010 | while (currentContext != null) { |
4011 | final ScrollableState? scrollableState = |
4012 | currentContext.findAncestorStateOfType<ScrollableState>(); |
4013 | if (scrollableState == notificationScrollableState) { |
4014 | return true; |
4015 | } |
4016 | currentContext = scrollableState?.context; |
4017 | } |
4018 | return false; |
4019 | } |
4020 | |
4021 | void _handleContextMenuOnParentScroll(ScrollNotification notification) { |
4022 | // Do some preliminary checks to avoid expensive subtree traversal. |
4023 | if (notification is! ScrollStartNotification && notification is! ScrollEndNotification) { |
4024 | return; |
4025 | } |
4026 | switch (notification) { |
4027 | case ScrollStartNotification() when _dataWhenToolbarShowScheduled != null: |
4028 | case ScrollEndNotification() when _dataWhenToolbarShowScheduled == null: |
4029 | break; |
4030 | case ScrollEndNotification() when _dataWhenToolbarShowScheduled!.value != _value: |
4031 | _dataWhenToolbarShowScheduled = null; |
4032 | _disposeScrollNotificationObserver(); |
4033 | case ScrollNotification(:final BuildContext? context) |
4034 | when !_isInternalScrollableNotification(context) && |
4035 | _scrollableNotificationIsFromSameSubtree(context): |
4036 | _handleContextMenuOnScroll(notification); |
4037 | } |
4038 | } |
4039 | |
4040 | Rect _calculateDeviceRect() { |
4041 | final Size screenSize = MediaQuery.sizeOf(context); |
4042 | final ui.FlutterView view = View.of(context); |
4043 | final double obscuredVertical = |
4044 | (view.padding.top + view.padding.bottom + view.viewInsets.bottom) / view.devicePixelRatio; |
4045 | final double obscuredHorizontal = |
4046 | (view.padding.left + view.padding.right) / view.devicePixelRatio; |
4047 | final Size visibleScreenSize = Size( |
4048 | screenSize.width - obscuredHorizontal, |
4049 | screenSize.height - obscuredVertical, |
4050 | ); |
4051 | return Rect.fromLTWH( |
4052 | view.padding.left / view.devicePixelRatio, |
4053 | view.padding.top / view.devicePixelRatio, |
4054 | visibleScreenSize.width, |
4055 | visibleScreenSize.height, |
4056 | ); |
4057 | } |
4058 | |
4059 | bool _showToolbarOnScreenScheduled = false; |
4060 | void _handleContextMenuOnScroll(ScrollNotification notification) { |
4061 | if (_webContextMenuEnabled) { |
4062 | return; |
4063 | } |
4064 | if (!_platformSupportsFadeOnScroll) { |
4065 | _selectionOverlay?.updateForScroll(); |
4066 | return; |
4067 | } |
4068 | // When the scroll begins and the toolbar is visible, hide it |
4069 | // until scrolling ends. |
4070 | // |
4071 | // The selection and renderEditable need to be visible within the current |
4072 | // viewport for the toolbar to show when scrolling ends. If they are not |
4073 | // then the toolbar is shown when they are scrolled back into view, unless |
4074 | // invalidated by a change in TextEditingValue. |
4075 | if (notification is ScrollStartNotification) { |
4076 | if (_dataWhenToolbarShowScheduled != null) { |
4077 | return; |
4078 | } |
4079 | final bool toolbarIsVisible = |
4080 | _selectionOverlay != null && |
4081 | _selectionOverlay!.toolbarIsVisible && |
4082 | !_selectionOverlay!.spellCheckToolbarIsVisible; |
4083 | if (!toolbarIsVisible) { |
4084 | return; |
4085 | } |
4086 | final List<TextBox> selectionBoxes = renderEditable.getBoxesForSelection(_value.selection); |
4087 | final Rect selectionBounds = |
4088 | _value.selection.isCollapsed || selectionBoxes.isEmpty |
4089 | ? renderEditable.getLocalRectForCaret(_value.selection.extent) |
4090 | : selectionBoxes |
4091 | .map((TextBox box) => box.toRect()) |
4092 | .reduce((Rect result, Rect rect) => result.expandToInclude(rect)); |
4093 | _dataWhenToolbarShowScheduled = (value: _value, selectionBounds: selectionBounds); |
4094 | _selectionOverlay?.hideToolbar(); |
4095 | } else if (notification is ScrollEndNotification) { |
4096 | if (_dataWhenToolbarShowScheduled == null) { |
4097 | return; |
4098 | } |
4099 | if (_dataWhenToolbarShowScheduled!.value != _value) { |
4100 | // Value has changed so we should invalidate any toolbar scheduling. |
4101 | _dataWhenToolbarShowScheduled = null; |
4102 | _disposeScrollNotificationObserver(); |
4103 | return; |
4104 | } |
4105 | |
4106 | if (_showToolbarOnScreenScheduled) { |
4107 | return; |
4108 | } |
4109 | _showToolbarOnScreenScheduled = true; |
4110 | SchedulerBinding.instance.addPostFrameCallback((Duration _) { |
4111 | _showToolbarOnScreenScheduled = false; |
4112 | if (!mounted) { |
4113 | return; |
4114 | } |
4115 | final Rect deviceRect = _calculateDeviceRect(); |
4116 | final bool selectionVisibleInEditable = |
4117 | renderEditable.selectionStartInViewport.value || |
4118 | renderEditable.selectionEndInViewport.value; |
4119 | final Rect selectionBounds = MatrixUtils.transformRect( |
4120 | renderEditable.getTransformTo(null), |
4121 | _dataWhenToolbarShowScheduled!.selectionBounds, |
4122 | ); |
4123 | final bool selectionOverlapsWithDeviceRect = |
4124 | !selectionBounds.hasNaN && deviceRect.overlaps(selectionBounds); |
4125 | |
4126 | if (selectionVisibleInEditable && |
4127 | selectionOverlapsWithDeviceRect && |
4128 | _selectionInViewport(_dataWhenToolbarShowScheduled!.selectionBounds)) { |
4129 | showToolbar(); |
4130 | _dataWhenToolbarShowScheduled = null; |
4131 | } |
4132 | }, debugLabel: 'EditableText.scheduleToolbar'); |
4133 | } |
4134 | } |
4135 | |
4136 | bool _selectionInViewport(Rect selectionBounds) { |
4137 | RenderAbstractViewport? closestViewport = RenderAbstractViewport.maybeOf(renderEditable); |
4138 | while (closestViewport != null) { |
4139 | final Rect selectionBoundsLocalToViewport = MatrixUtils.transformRect( |
4140 | renderEditable.getTransformTo(closestViewport), |
4141 | selectionBounds, |
4142 | ); |
4143 | if (selectionBoundsLocalToViewport.hasNaN || |
4144 | closestViewport.paintBounds.hasNaN || |
4145 | !closestViewport.paintBounds.overlaps(selectionBoundsLocalToViewport)) { |
4146 | return false; |
4147 | } |
4148 | closestViewport = RenderAbstractViewport.maybeOf(closestViewport.parent); |
4149 | } |
4150 | return true; |
4151 | } |
4152 | |
4153 | TextSelectionOverlay _createSelectionOverlay() { |
4154 | final EditableTextContextMenuBuilder? contextMenuBuilder = widget.contextMenuBuilder; |
4155 | final TextSelectionOverlay selectionOverlay = TextSelectionOverlay( |
4156 | clipboardStatus: clipboardStatus, |
4157 | context: context, |
4158 | value: _value, |
4159 | debugRequiredFor: widget, |
4160 | toolbarLayerLink: _toolbarLayerLink, |
4161 | startHandleLayerLink: _startHandleLayerLink, |
4162 | endHandleLayerLink: _endHandleLayerLink, |
4163 | renderObject: renderEditable, |
4164 | selectionControls: widget.selectionControls, |
4165 | selectionDelegate: this, |
4166 | dragStartBehavior: widget.dragStartBehavior, |
4167 | onSelectionHandleTapped: widget.onSelectionHandleTapped, |
4168 | contextMenuBuilder: |
4169 | contextMenuBuilder == null || _webContextMenuEnabled |
4170 | ? null |
4171 | : (BuildContext context) { |
4172 | return contextMenuBuilder(context, this); |
4173 | }, |
4174 | magnifierConfiguration: widget.magnifierConfiguration, |
4175 | ); |
4176 | |
4177 | return selectionOverlay; |
4178 | } |
4179 | |
4180 | @pragma('vm:notify-debugger-on-exception') |
4181 | void _handleSelectionChanged(TextSelection selection, SelectionChangedCause? cause) { |
4182 | // We return early if the selection is not valid. This can happen when the |
4183 | // text of [EditableText] is updated at the same time as the selection is |
4184 | // changed by a gesture event. |
4185 | final String text = widget.controller.value.text; |
4186 | if (text.length < selection.end || text.length < selection.start) { |
4187 | return; |
4188 | } |
4189 | |
4190 | widget.controller.selection = selection; |
4191 | |
4192 | // This will show the keyboard for all selection changes on the |
4193 | // EditableText except for those triggered by a keyboard input. |
4194 | // Typically EditableText shouldn't take user keyboard input if |
4195 | // it's not focused already. If the EditableText is being |
4196 | // autofilled it shouldn't request focus. |
4197 | switch (cause) { |
4198 | case null: |
4199 | case SelectionChangedCause.doubleTap: |
4200 | case SelectionChangedCause.drag: |
4201 | case SelectionChangedCause.forcePress: |
4202 | case SelectionChangedCause.longPress: |
4203 | case SelectionChangedCause.stylusHandwriting: |
4204 | case SelectionChangedCause.tap: |
4205 | case SelectionChangedCause.toolbar: |
4206 | requestKeyboard(); |
4207 | case SelectionChangedCause.keyboard: |
4208 | } |
4209 | if (widget.selectionControls == null && widget.contextMenuBuilder == null) { |
4210 | _selectionOverlay?.dispose(); |
4211 | _selectionOverlay = null; |
4212 | } else { |
4213 | if (_selectionOverlay == null) { |
4214 | _selectionOverlay = _createSelectionOverlay(); |
4215 | } else { |
4216 | _selectionOverlay!.update(_value); |
4217 | } |
4218 | _selectionOverlay!.handlesVisible = widget.showSelectionHandles; |
4219 | _selectionOverlay!.showHandles(); |
4220 | } |
4221 | // TODO(chunhtai): we should make sure selection actually changed before |
4222 | // we call the onSelectionChanged. |
4223 | // https://github.com/flutter/flutter/issues/76349. |
4224 | try { |
4225 | widget.onSelectionChanged?.call(selection, cause); |
4226 | } catch (exception, stack) { |
4227 | FlutterError.reportError( |
4228 | FlutterErrorDetails( |
4229 | exception: exception, |
4230 | stack: stack, |
4231 | library: 'widgets', |
4232 | context: ErrorDescription('while calling onSelectionChanged for $cause'), |
4233 | ), |
4234 | ); |
4235 | } |
4236 | |
4237 | // To keep the cursor from blinking while it moves, restart the timer here. |
4238 | if (_showBlinkingCursor && _cursorTimer != null) { |
4239 | _stopCursorBlink(resetCharTicks: false); |
4240 | _startCursorBlink(); |
4241 | } |
4242 | } |
4243 | |
4244 | // Animation configuration for scrolling the caret back on screen. |
4245 | static const Duration _caretAnimationDuration = Duration(milliseconds: 100); |
4246 | static const Curve _caretAnimationCurve = Curves.fastOutSlowIn; |
4247 | |
4248 | bool _showCaretOnScreenScheduled = false; |
4249 | |
4250 | void _scheduleShowCaretOnScreen({required bool withAnimation}) { |
4251 | if (_showCaretOnScreenScheduled) { |
4252 | return; |
4253 | } |
4254 | _showCaretOnScreenScheduled = true; |
4255 | SchedulerBinding.instance.addPostFrameCallback((Duration _) { |
4256 | _showCaretOnScreenScheduled = false; |
4257 | // Since we are in a post frame callback, check currentContext in case |
4258 | // RenderEditable has been disposed (in which case it will be null). |
4259 | final RenderEditable? renderEditable = |
4260 | _editableKey.currentContext?.findRenderObject() as RenderEditable?; |
4261 | if (renderEditable == null || |
4262 | !(renderEditable.selection?.isValid ?? false) || |
4263 | !_scrollController.hasClients) { |
4264 | return; |
4265 | } |
4266 | |
4267 | final double lineHeight = renderEditable.preferredLineHeight; |
4268 | |
4269 | // Enlarge the target rect by scrollPadding to ensure that caret is not |
4270 | // positioned directly at the edge after scrolling. |
4271 | double bottomSpacing = widget.scrollPadding.bottom; |
4272 | if (_selectionOverlay?.selectionControls != null) { |
4273 | final double handleHeight = |
4274 | _selectionOverlay!.selectionControls!.getHandleSize(lineHeight).height; |
4275 | final double interactiveHandleHeight = math.max(handleHeight, kMinInteractiveDimension); |
4276 | final Offset anchor = _selectionOverlay!.selectionControls!.getHandleAnchor( |
4277 | TextSelectionHandleType.collapsed, |
4278 | lineHeight, |
4279 | ); |
4280 | final double handleCenter = handleHeight / 2 - anchor.dy; |
4281 | bottomSpacing = math.max(handleCenter + interactiveHandleHeight / 2, bottomSpacing); |
4282 | } |
4283 | |
4284 | final EdgeInsets caretPadding = widget.scrollPadding.copyWith(bottom: bottomSpacing); |
4285 | |
4286 | final Rect caretRect = renderEditable.getLocalRectForCaret(renderEditable.selection!.extent); |
4287 | final RevealedOffset targetOffset = _getOffsetToRevealCaret(caretRect); |
4288 | |
4289 | final Rect rectToReveal; |
4290 | final TextSelection selection = textEditingValue.selection; |
4291 | if (selection.isCollapsed) { |
4292 | rectToReveal = targetOffset.rect; |
4293 | } else { |
4294 | final List<TextBox> selectionBoxes = renderEditable.getBoxesForSelection(selection); |
4295 | // selectionBoxes may be empty if, for example, the selection does not |
4296 | // encompass a full character, like if it only contained part of an |
4297 | // extended grapheme cluster. |
4298 | if (selectionBoxes.isEmpty) { |
4299 | rectToReveal = targetOffset.rect; |
4300 | } else { |
4301 | rectToReveal = |
4302 | selection.baseOffset < selection.extentOffset |
4303 | ? selectionBoxes.last.toRect() |
4304 | : selectionBoxes.first.toRect(); |
4305 | } |
4306 | } |
4307 | |
4308 | if (withAnimation) { |
4309 | _scrollController.animateTo( |
4310 | targetOffset.offset, |
4311 | duration: _caretAnimationDuration, |
4312 | curve: _caretAnimationCurve, |
4313 | ); |
4314 | renderEditable.showOnScreen( |
4315 | rect: caretPadding.inflateRect(rectToReveal), |
4316 | duration: _caretAnimationDuration, |
4317 | curve: _caretAnimationCurve, |
4318 | ); |
4319 | } else { |
4320 | _scrollController.jumpTo(targetOffset.offset); |
4321 | renderEditable.showOnScreen(rect: caretPadding.inflateRect(rectToReveal)); |
4322 | } |
4323 | }, debugLabel: 'EditableText.showCaret'); |
4324 | } |
4325 | |
4326 | late double _lastBottomViewInset; |
4327 | |
4328 | @override |
4329 | void didChangeMetrics() { |
4330 | if (!mounted) { |
4331 | return; |
4332 | } |
4333 | final ui.FlutterView view = View.of(context); |
4334 | if (_lastBottomViewInset != view.viewInsets.bottom) { |
4335 | SchedulerBinding.instance.addPostFrameCallback((Duration _) { |
4336 | _selectionOverlay?.updateForScroll(); |
4337 | }, debugLabel: 'EditableText.updateForScroll'); |
4338 | if (_lastBottomViewInset < view.viewInsets.bottom) { |
4339 | // Because the metrics change signal from engine will come here every frame |
4340 | // (on both iOS and Android). So we don't need to show caret with animation. |
4341 | _scheduleShowCaretOnScreen(withAnimation: false); |
4342 | } |
4343 | } |
4344 | _lastBottomViewInset = view.viewInsets.bottom; |
4345 | } |
4346 | |
4347 | Future<void> _performSpellCheck(final String text) async { |
4348 | try { |
4349 | final Locale? localeForSpellChecking = widget.locale ?? Localizations.maybeLocaleOf(context); |
4350 | |
4351 | assert( |
4352 | localeForSpellChecking != null, |
4353 | 'Locale must be specified in widget or Localization widget must be in scope', |
4354 | ); |
4355 | |
4356 | final List<SuggestionSpan>? suggestions = await _spellCheckConfiguration.spellCheckService! |
4357 | .fetchSpellCheckSuggestions(localeForSpellChecking!, text); |
4358 | |
4359 | if (suggestions == null) { |
4360 | // The request to fetch spell check suggestions was canceled due to ongoing request. |
4361 | return; |
4362 | } |
4363 | |
4364 | spellCheckResults = SpellCheckResults(text, suggestions); |
4365 | renderEditable.text = buildTextSpan(); |
4366 | } catch (exception, stack) { |
4367 | FlutterError.reportError( |
4368 | FlutterErrorDetails( |
4369 | exception: exception, |
4370 | stack: stack, |
4371 | library: 'widgets', |
4372 | context: ErrorDescription('while performing spell check'), |
4373 | ), |
4374 | ); |
4375 | } |
4376 | } |
4377 | |
4378 | @pragma('vm:notify-debugger-on-exception') |
4379 | void _formatAndSetValue( |
4380 | TextEditingValue value, |
4381 | SelectionChangedCause? cause, { |
4382 | bool userInteraction = false, |
4383 | }) { |
4384 | final TextEditingValue oldValue = _value; |
4385 | final bool textChanged = oldValue.text != value.text; |
4386 | final bool textCommitted = !oldValue.composing.isCollapsed && value.composing.isCollapsed; |
4387 | final bool selectionChanged = oldValue.selection != value.selection; |
4388 | |
4389 | if (textChanged || textCommitted) { |
4390 | // Only apply input formatters if the text has changed (including uncommitted |
4391 | // text in the composing region), or when the user committed the composing |
4392 | // text. |
4393 | // Gboard is very persistent in restoring the composing region. Applying |
4394 | // input formatters on composing-region-only changes (except clearing the |
4395 | // current composing region) is very infinite-loop-prone: the formatters |
4396 | // will keep trying to modify the composing region while Gboard will keep |
4397 | // trying to restore the original composing region. |
4398 | try { |
4399 | value = |
4400 | widget.inputFormatters?.fold<TextEditingValue>( |
4401 | value, |
4402 | (TextEditingValue newValue, TextInputFormatter formatter) => |
4403 | formatter.formatEditUpdate(_value, newValue), |
4404 | ) ?? |
4405 | value; |
4406 | |
4407 | if (spellCheckEnabled && value.text.isNotEmpty && _value.text != value.text) { |
4408 | _performSpellCheck(value.text); |
4409 | } |
4410 | } catch (exception, stack) { |
4411 | FlutterError.reportError( |
4412 | FlutterErrorDetails( |
4413 | exception: exception, |
4414 | stack: stack, |
4415 | library: 'widgets', |
4416 | context: ErrorDescription('while applying input formatters'), |
4417 | ), |
4418 | ); |
4419 | } |
4420 | } |
4421 | |
4422 | final TextSelection oldTextSelection = textEditingValue.selection; |
4423 | |
4424 | // Put all optional user callback invocations in a batch edit to prevent |
4425 | // sending multiple `TextInput.updateEditingValue` messages. |
4426 | beginBatchEdit(); |
4427 | _value = value; |
4428 | // Changes made by the keyboard can sometimes be "out of band" for listening |
4429 | // components, so always send those events, even if we didn't think it |
4430 | // changed. Also, the user long pressing should always send a selection change |
4431 | // as well. |
4432 | if (selectionChanged || |
4433 | (userInteraction && |
4434 | (cause == SelectionChangedCause.longPress || |
4435 | cause == SelectionChangedCause.keyboard))) { |
4436 | _handleSelectionChanged(_value.selection, cause); |
4437 | _bringIntoViewBySelectionState(oldTextSelection, value.selection, cause); |
4438 | } |
4439 | final String currentText = _value.text; |
4440 | if (oldValue.text != currentText) { |
4441 | try { |
4442 | widget.onChanged?.call(currentText); |
4443 | } catch (exception, stack) { |
4444 | FlutterError.reportError( |
4445 | FlutterErrorDetails( |
4446 | exception: exception, |
4447 | stack: stack, |
4448 | library: 'widgets', |
4449 | context: ErrorDescription('while calling onChanged'), |
4450 | ), |
4451 | ); |
4452 | } |
4453 | } |
4454 | endBatchEdit(); |
4455 | } |
4456 | |
4457 | void _bringIntoViewBySelectionState( |
4458 | TextSelection oldSelection, |
4459 | TextSelection newSelection, |
4460 | SelectionChangedCause? cause, |
4461 | ) { |
4462 | switch (defaultTargetPlatform) { |
4463 | case TargetPlatform.iOS: |
4464 | case TargetPlatform.macOS: |
4465 | if (cause == SelectionChangedCause.longPress || cause == SelectionChangedCause.drag) { |
4466 | bringIntoView(newSelection.extent); |
4467 | } |
4468 | case TargetPlatform.linux: |
4469 | case TargetPlatform.windows: |
4470 | case TargetPlatform.fuchsia: |
4471 | case TargetPlatform.android: |
4472 | if (cause == SelectionChangedCause.drag) { |
4473 | if (oldSelection.baseOffset != newSelection.baseOffset) { |
4474 | bringIntoView(newSelection.base); |
4475 | } else if (oldSelection.extentOffset != newSelection.extentOffset) { |
4476 | bringIntoView(newSelection.extent); |
4477 | } |
4478 | } |
4479 | } |
4480 | } |
4481 | |
4482 | void _onCursorColorTick() { |
4483 | final double effectiveOpacity = math.min( |
4484 | widget.cursorColor.alpha / 255.0, |
4485 | _cursorBlinkOpacityController.value, |
4486 | ); |
4487 | renderEditable.cursorColor = widget.cursorColor.withOpacity(effectiveOpacity); |
4488 | _cursorVisibilityNotifier.value = |
4489 | widget.showCursor && |
4490 | (EditableText.debugDeterministicCursor || _cursorBlinkOpacityController.value > 0); |
4491 | } |
4492 | |
4493 | bool get _showBlinkingCursor => |
4494 | _hasFocus && |
4495 | _value.selection.isCollapsed && |
4496 | widget.showCursor && |
4497 | _tickersEnabled && |
4498 | !renderEditable.floatingCursorOn; |
4499 | |
4500 | /// Whether the blinking cursor is actually visible at this precise moment |
4501 | /// (it's hidden half the time, since it blinks). |
4502 | @visibleForTesting |
4503 | bool get cursorCurrentlyVisible => _cursorBlinkOpacityController.value > 0; |
4504 | |
4505 | /// The cursor blink interval (the amount of time the cursor is in the "on" |
4506 | /// state or the "off" state). A complete cursor blink period is twice this |
4507 | /// value (half on, half off). |
4508 | @visibleForTesting |
4509 | Duration get cursorBlinkInterval => _kCursorBlinkHalfPeriod; |
4510 | |
4511 | /// The current status of the text selection handles. |
4512 | @visibleForTesting |
4513 | TextSelectionOverlay? get selectionOverlay => _selectionOverlay; |
4514 | |
4515 | int _obscureShowCharTicksPending = 0; |
4516 | int? _obscureLatestCharIndex; |
4517 | |
4518 | void _startCursorBlink() { |
4519 | assert( |
4520 | !(_cursorTimer?.isActive ?? false) || |
4521 | !(_backingCursorBlinkOpacityController?.isAnimating ?? false), |
4522 | ); |
4523 | if (!widget.showCursor) { |
4524 | return; |
4525 | } |
4526 | if (!_tickersEnabled) { |
4527 | return; |
4528 | } |
4529 | _cursorTimer?.cancel(); |
4530 | _cursorBlinkOpacityController.value = 1.0; |
4531 | if (EditableText.debugDeterministicCursor) { |
4532 | return; |
4533 | } |
4534 | if (widget.cursorOpacityAnimates) { |
4535 | _cursorBlinkOpacityController |
4536 | .animateWith(_iosBlinkCursorSimulation) |
4537 | .whenComplete(_onCursorTick); |
4538 | } else { |
4539 | _cursorTimer = Timer.periodic(_kCursorBlinkHalfPeriod, (Timer timer) { |
4540 | _onCursorTick(); |
4541 | }); |
4542 | } |
4543 | } |
4544 | |
4545 | void _onCursorTick() { |
4546 | if (_obscureShowCharTicksPending > 0) { |
4547 | _obscureShowCharTicksPending = |
4548 | WidgetsBinding.instance.platformDispatcher.brieflyShowPassword |
4549 | ? _obscureShowCharTicksPending - 1 |
4550 | : 0; |
4551 | if (_obscureShowCharTicksPending == 0) { |
4552 | setState(() {}); |
4553 | } |
4554 | } |
4555 | |
4556 | if (widget.cursorOpacityAnimates) { |
4557 | _cursorTimer?.cancel(); |
4558 | // Schedule this as an async task to avoid blocking tester.pumpAndSettle |
4559 | // indefinitely. |
4560 | _cursorTimer = Timer( |
4561 | Duration.zero, |
4562 | () => _cursorBlinkOpacityController |
4563 | .animateWith(_iosBlinkCursorSimulation) |
4564 | .whenComplete(_onCursorTick), |
4565 | ); |
4566 | } else { |
4567 | if (!(_cursorTimer?.isActive ?? false) && _tickersEnabled) { |
4568 | _cursorTimer = Timer.periodic(_kCursorBlinkHalfPeriod, (Timer timer) { |
4569 | _onCursorTick(); |
4570 | }); |
4571 | } |
4572 | _cursorBlinkOpacityController.value = _cursorBlinkOpacityController.value == 0 ? 1 : 0; |
4573 | } |
4574 | } |
4575 | |
4576 | void _stopCursorBlink({bool resetCharTicks = true}) { |
4577 | // If the cursor is animating, stop the animation, and we always |
4578 | // want the cursor to be visible when the floating cursor is enabled. |
4579 | _cursorBlinkOpacityController.value = renderEditable.floatingCursorOn ? 1.0 : 0.0; |
4580 | _cursorTimer?.cancel(); |
4581 | _cursorTimer = null; |
4582 | if (resetCharTicks) { |
4583 | _obscureShowCharTicksPending = 0; |
4584 | } |
4585 | } |
4586 | |
4587 | void _startOrStopCursorTimerIfNeeded() { |
4588 | if (!_showBlinkingCursor) { |
4589 | _stopCursorBlink(); |
4590 | } else if (_cursorTimer == null) { |
4591 | _startCursorBlink(); |
4592 | } |
4593 | } |
4594 | |
4595 | void _didChangeTextEditingValue() { |
4596 | if (_hasFocus && !_value.selection.isValid) { |
4597 | // If this field is focused and the selection is invalid, place the cursor at |
4598 | // the end. Does not rely on _handleFocusChanged because it makes selection |
4599 | // handles visible on Android. |
4600 | // Unregister as a listener to the text controller while making the change. |
4601 | widget.controller.removeListener(_didChangeTextEditingValue); |
4602 | widget.controller.selection = _adjustedSelectionWhenFocused()!; |
4603 | widget.controller.addListener(_didChangeTextEditingValue); |
4604 | } |
4605 | _updateRemoteEditingValueIfNeeded(); |
4606 | _startOrStopCursorTimerIfNeeded(); |
4607 | _updateOrDisposeSelectionOverlayIfNeeded(); |
4608 | // TODO(abarth): Teach RenderEditable about ValueNotifier |
4609 | // to avoid this setState(). |
4610 | setState(() { |
4611 | /* We use widget.controller.value in build(). */ |
4612 | }); |
4613 | _verticalSelectionUpdateAction.stopCurrentVerticalRunIfSelectionChanges(); |
4614 | } |
4615 | |
4616 | void _handleFocusChanged() { |
4617 | _openOrCloseInputConnectionIfNeeded(); |
4618 | _startOrStopCursorTimerIfNeeded(); |
4619 | _updateOrDisposeSelectionOverlayIfNeeded(); |
4620 | if (_hasFocus) { |
4621 | // Listen for changing viewInsets, which indicates keyboard showing up. |
4622 | WidgetsBinding.instance.addObserver(this); |
4623 | _lastBottomViewInset = View.of(context).viewInsets.bottom; |
4624 | if (!widget.readOnly) { |
4625 | _scheduleShowCaretOnScreen(withAnimation: true); |
4626 | } |
4627 | final TextSelection? updatedSelection = _adjustedSelectionWhenFocused(); |
4628 | if (updatedSelection != null) { |
4629 | _handleSelectionChanged(updatedSelection, null); |
4630 | } |
4631 | } else { |
4632 | WidgetsBinding.instance.removeObserver(this); |
4633 | setState(() { |
4634 | _currentPromptRectRange = null; |
4635 | }); |
4636 | } |
4637 | updateKeepAlive(); |
4638 | } |
4639 | |
4640 | TextSelection? _adjustedSelectionWhenFocused() { |
4641 | TextSelection? selection; |
4642 | final bool isDesktop = switch (defaultTargetPlatform) { |
4643 | TargetPlatform.android || TargetPlatform.iOS || TargetPlatform.fuchsia => false, |
4644 | TargetPlatform.macOS || TargetPlatform.linux || TargetPlatform.windows => true, |
4645 | }; |
4646 | final bool shouldSelectAll = |
4647 | widget.selectionEnabled && |
4648 | (kIsWeb || isDesktop) && |
4649 | !_isMultiline && |
4650 | !_nextFocusChangeIsInternal && |
4651 | !_justResumed; |
4652 | _justResumed = false; |
4653 | if (shouldSelectAll) { |
4654 | // On native web and desktop platforms, single line tags |
4655 | // select all when receiving focus. |
4656 | selection = TextSelection(baseOffset: 0, extentOffset: _value.text.length); |
4657 | } else if (!_value.selection.isValid) { |
4658 | // Place cursor at the end if the selection is invalid when we receive focus. |
4659 | selection = TextSelection.collapsed(offset: _value.text.length); |
4660 | } |
4661 | return selection; |
4662 | } |
4663 | |
4664 | void _compositeCallback(Layer layer) { |
4665 | // The callback can be invoked when the layer is detached. |
4666 | // The input connection can be closed by the platform in which case this |
4667 | // widget doesn't rebuild. |
4668 | if (!renderEditable.attached || !_hasInputConnection) { |
4669 | return; |
4670 | } |
4671 | assert(mounted); |
4672 | assert((context as Element).debugIsActive); |
4673 | _updateSizeAndTransform(); |
4674 | } |
4675 | |
4676 | // Must be called after layout. |
4677 | // See https://github.com/flutter/flutter/issues/126312 |
4678 | void _updateSizeAndTransform() { |
4679 | final Size size = renderEditable.size; |
4680 | final Matrix4 transform = renderEditable.getTransformTo(null); |
4681 | _textInputConnection!.setEditableSizeAndTransform(size, transform); |
4682 | } |
4683 | |
4684 | void _schedulePeriodicPostFrameCallbacks([Duration? duration]) { |
4685 | if (!_hasInputConnection) { |
4686 | return; |
4687 | } |
4688 | _updateSelectionRects(); |
4689 | _updateComposingRectIfNeeded(); |
4690 | _updateCaretRectIfNeeded(); |
4691 | SchedulerBinding.instance.addPostFrameCallback( |
4692 | _schedulePeriodicPostFrameCallbacks, |
4693 | debugLabel: 'EditableText.postFrameCallbacks', |
4694 | ); |
4695 | } |
4696 | |
4697 | _ScribbleCacheKey? _scribbleCacheKey; |
4698 | |
4699 | void _updateSelectionRects({bool force = false}) { |
4700 | if (!_stylusHandwritingEnabled || defaultTargetPlatform != TargetPlatform.iOS) { |
4701 | return; |
4702 | } |
4703 | |
4704 | final ScrollDirection scrollDirection = _scrollController.position.userScrollDirection; |
4705 | if (scrollDirection != ScrollDirection.idle) { |
4706 | return; |
4707 | } |
4708 | |
4709 | final InlineSpan inlineSpan = renderEditable.text!; |
4710 | final TextScaler effectiveTextScaler = switch ((widget.textScaler, widget.textScaleFactor)) { |
4711 | (final TextScaler textScaler, _) => textScaler, |
4712 | (null, final double textScaleFactor) => TextScaler.linear(textScaleFactor), |
4713 | (null, null) => MediaQuery.textScalerOf(context), |
4714 | }; |
4715 | |
4716 | final _ScribbleCacheKey newCacheKey = _ScribbleCacheKey( |
4717 | inlineSpan: inlineSpan, |
4718 | textAlign: widget.textAlign, |
4719 | textDirection: _textDirection, |
4720 | textScaler: effectiveTextScaler, |
4721 | textHeightBehavior: widget.textHeightBehavior ?? DefaultTextHeightBehavior.maybeOf(context), |
4722 | locale: widget.locale, |
4723 | structStyle: widget.strutStyle, |
4724 | placeholder: _placeholderLocation, |
4725 | size: renderEditable.size, |
4726 | ); |
4727 | |
4728 | final RenderComparison comparison = |
4729 | force |
4730 | ? RenderComparison.layout |
4731 | : _scribbleCacheKey?.compare(newCacheKey) ?? RenderComparison.layout; |
4732 | if (comparison.index < RenderComparison.layout.index) { |
4733 | return; |
4734 | } |
4735 | _scribbleCacheKey = newCacheKey; |
4736 | |
4737 | final List<SelectionRect> rects = <SelectionRect>[]; |
4738 | int graphemeStart = 0; |
4739 | // Can't use _value.text here: the controller value could change between |
4740 | // frames. |
4741 | final String plainText = inlineSpan.toPlainText(includeSemanticsLabels: false); |
4742 | final CharacterRange characterRange = CharacterRange(plainText); |
4743 | while (characterRange.moveNext()) { |
4744 | final int graphemeEnd = graphemeStart + characterRange.current.length; |
4745 | final List<TextBox> boxes = renderEditable.getBoxesForSelection( |
4746 | TextSelection(baseOffset: graphemeStart, extentOffset: graphemeEnd), |
4747 | ); |
4748 | |
4749 | final TextBox? box = boxes.isEmpty ? null : boxes.first; |
4750 | if (box != null) { |
4751 | final Rect paintBounds = renderEditable.paintBounds; |
4752 | // Stop early when characters are already below the bottom edge of the |
4753 | // RenderEditable, regardless of its clipBehavior. |
4754 | if (paintBounds.bottom <= box.top) { |
4755 | break; |
4756 | } |
4757 | // Include any TextBox which intersects with the RenderEditable. |
4758 | if (paintBounds.left <= box.right && |
4759 | box.left <= paintBounds.right && |
4760 | paintBounds.top <= box.bottom) { |
4761 | // At least some part of the letter is visible within the text field. |
4762 | rects.add( |
4763 | SelectionRect(position: graphemeStart, bounds: box.toRect(), direction: box.direction), |
4764 | ); |
4765 | } |
4766 | } |
4767 | graphemeStart = graphemeEnd; |
4768 | } |
4769 | _textInputConnection!.setSelectionRects(rects); |
4770 | } |
4771 | |
4772 | // Sends the current composing rect to the embedder's text input plugin. |
4773 | // |
4774 | // In cases where the composing rect hasn't been updated in the embedder due |
4775 | // to the lag of asynchronous messages over the channel, the position of the |
4776 | // current caret rect is used instead. |
4777 | // |
4778 | // See: [_updateCaretRectIfNeeded] |
4779 | void _updateComposingRectIfNeeded() { |
4780 | final TextRange composingRange = _value.composing; |
4781 | assert(mounted); |
4782 | Rect? composingRect = renderEditable.getRectForComposingRange(composingRange); |
4783 | // Send the caret location instead if there's no marked text yet. |
4784 | if (composingRect == null) { |
4785 | final int offset = composingRange.isValid ? composingRange.start : 0; |
4786 | composingRect = renderEditable.getLocalRectForCaret(TextPosition(offset: offset)); |
4787 | } |
4788 | _textInputConnection!.setComposingRect(composingRect); |
4789 | } |
4790 | |
4791 | // Sends the current caret rect to the embedder's text input plugin. |
4792 | // |
4793 | // The position of the caret rect is updated periodically such that if the |
4794 | // user initiates composing input, the current cursor rect can be used for |
4795 | // the first character until the composing rect can be sent. |
4796 | // |
4797 | // On selection changes, the start of the selection is used. This ensures |
4798 | // that regardless of the direction the selection was created, the cursor is |
4799 | // set to the position where next text input occurs. This position is used to |
4800 | // position the IME's candidate selection menu. |
4801 | // |
4802 | // See: [_updateComposingRectIfNeeded] |
4803 | void _updateCaretRectIfNeeded() { |
4804 | final TextSelection? selection = renderEditable.selection; |
4805 | if (selection == null || !selection.isValid) { |
4806 | return; |
4807 | } |
4808 | final TextPosition currentTextPosition = TextPosition(offset: selection.start); |
4809 | final Rect caretRect = renderEditable.getLocalRectForCaret(currentTextPosition); |
4810 | _textInputConnection!.setCaretRect(caretRect); |
4811 | } |
4812 | |
4813 | TextDirection get _textDirection => widget.textDirection ?? Directionality.of(context); |
4814 | |
4815 | /// The renderer for this widget's descendant. |
4816 | /// |
4817 | /// This property is typically used to notify the renderer of input gestures |
4818 | /// when [RenderEditable.ignorePointer] is true. |
4819 | late final RenderEditable renderEditable = |
4820 | _editableKey.currentContext!.findRenderObject()! as RenderEditable; |
4821 | |
4822 | @override |
4823 | TextEditingValue get textEditingValue => _value; |
4824 | |
4825 | double get _devicePixelRatio => MediaQuery.devicePixelRatioOf(context); |
4826 | |
4827 | @override |
4828 | void userUpdateTextEditingValue(TextEditingValue value, SelectionChangedCause? cause) { |
4829 | // Compare the current TextEditingValue with the pre-format new |
4830 | // TextEditingValue value, in case the formatter would reject the change. |
4831 | final bool shouldShowCaret = |
4832 | widget.readOnly ? _value.selection != value.selection : _value != value; |
4833 | if (shouldShowCaret) { |
4834 | _scheduleShowCaretOnScreen(withAnimation: true); |
4835 | } |
4836 | |
4837 | // Even if the value doesn't change, it may be necessary to focus and build |
4838 | // the selection overlay. For example, this happens when right clicking an |
4839 | // unfocused field that previously had a selection in the same spot. |
4840 | if (value == textEditingValue) { |
4841 | if (!widget.focusNode.hasFocus) { |
4842 | _flagInternalFocus(); |
4843 | widget.focusNode.requestFocus(); |
4844 | _selectionOverlay ??= _createSelectionOverlay(); |
4845 | } |
4846 | return; |
4847 | } |
4848 | |
4849 | _formatAndSetValue(value, cause, userInteraction: true); |
4850 | } |
4851 | |
4852 | @override |
4853 | void bringIntoView(TextPosition position) { |
4854 | final Rect localRect = renderEditable.getLocalRectForCaret(position); |
4855 | final RevealedOffset targetOffset = _getOffsetToRevealCaret(localRect); |
4856 | |
4857 | _scrollController.jumpTo(targetOffset.offset); |
4858 | renderEditable.showOnScreen(rect: targetOffset.rect); |
4859 | } |
4860 | |
4861 | /// Shows the selection toolbar at the location of the current cursor. |
4862 | /// |
4863 | /// Returns `false` if a toolbar couldn't be shown, such as when the toolbar |
4864 | /// is already shown, or when no text selection currently exists. |
4865 | @override |
4866 | bool showToolbar() { |
4867 | // Web is using native dom elements to enable clipboard functionality of the |
4868 | // context menu: copy, paste, select, cut. It might also provide additional |
4869 | // functionality depending on the browser (such as translate). Due to this, |
4870 | // we should not show a Flutter toolbar for the editable text elements |
4871 | // unless the browser's context menu is explicitly disabled. |
4872 | if (_webContextMenuEnabled) { |
4873 | return false; |
4874 | } |
4875 | |
4876 | if (_selectionOverlay == null) { |
4877 | return false; |
4878 | } |
4879 | if (_selectionOverlay!.toolbarIsVisible) { |
4880 | return false; |
4881 | } |
4882 | _liveTextInputStatus?.update(); |
4883 | clipboardStatus.update(); |
4884 | _selectionOverlay!.showToolbar(); |
4885 | // Listen to parent scroll events when the toolbar is visible so it can be |
4886 | // hidden during a scroll on supported platforms. |
4887 | if (_platformSupportsFadeOnScroll) { |
4888 | _listeningToScrollNotificationObserver = true; |
4889 | _scrollNotificationObserver?.removeListener(_handleContextMenuOnParentScroll); |
4890 | _scrollNotificationObserver = ScrollNotificationObserver.maybeOf(context); |
4891 | _scrollNotificationObserver?.addListener(_handleContextMenuOnParentScroll); |
4892 | } |
4893 | return true; |
4894 | } |
4895 | |
4896 | @override |
4897 | void hideToolbar([bool hideHandles = true]) { |
4898 | // Stop listening to parent scroll events when toolbar is hidden. |
4899 | _disposeScrollNotificationObserver(); |
4900 | if (hideHandles) { |
4901 | // Hide the handles and the toolbar. |
4902 | _selectionOverlay?.hide(); |
4903 | } else if (_selectionOverlay?.toolbarIsVisible ?? false) { |
4904 | // Hide only the toolbar but not the handles. |
4905 | _selectionOverlay?.hideToolbar(); |
4906 | } |
4907 | } |
4908 | |
4909 | /// Toggles the visibility of the toolbar. |
4910 | void toggleToolbar([bool hideHandles = true]) { |
4911 | final TextSelectionOverlay selectionOverlay = _selectionOverlay ??= _createSelectionOverlay(); |
4912 | if (selectionOverlay.toolbarIsVisible) { |
4913 | hideToolbar(hideHandles); |
4914 | } else { |
4915 | showToolbar(); |
4916 | } |
4917 | } |
4918 | |
4919 | /// Shows toolbar with spell check suggestions of misspelled words that are |
4920 | /// available for click-and-replace. |
4921 | bool showSpellCheckSuggestionsToolbar() { |
4922 | // Spell check suggestions toolbars are intended to be shown on non-web |
4923 | // platforms. Additionally, the Cupertino style toolbar can't be drawn on |
4924 | // the web with the HTML renderer due to |
4925 | // https://github.com/flutter/flutter/issues/123560. |
4926 | if (!spellCheckEnabled || |
4927 | _webContextMenuEnabled || |
4928 | widget.readOnly || |
4929 | _selectionOverlay == null || |
4930 | !_spellCheckResultsReceived || |
4931 | findSuggestionSpanAtCursorIndex(textEditingValue.selection.extentOffset) == null) { |
4932 | // Only attempt to show the spell check suggestions toolbar if there |
4933 | // is a toolbar specified and spell check suggestions available to show. |
4934 | return false; |
4935 | } |
4936 | |
4937 | assert( |
4938 | _spellCheckConfiguration.spellCheckSuggestionsToolbarBuilder != null, |
4939 | 'spellCheckSuggestionsToolbarBuilder must be defined in ' |
4940 | 'SpellCheckConfiguration to show a toolbar with spell check ' |
4941 | 'suggestions', |
4942 | ); |
4943 | |
4944 | _selectionOverlay!.showSpellCheckSuggestionsToolbar((BuildContext context) { |
4945 | return _spellCheckConfiguration.spellCheckSuggestionsToolbarBuilder!(context, this); |
4946 | }); |
4947 | return true; |
4948 | } |
4949 | |
4950 | /// Shows the magnifier at the position given by `positionToShow`, |
4951 | /// if there is no magnifier visible. |
4952 | /// |
4953 | /// Updates the magnifier to the position given by `positionToShow`, |
4954 | /// if there is a magnifier visible. |
4955 | /// |
4956 | /// Does nothing if a magnifier couldn't be shown, such as when the selection |
4957 | /// overlay does not currently exist. |
4958 | void showMagnifier(Offset positionToShow) { |
4959 | if (_selectionOverlay == null) { |
4960 | return; |
4961 | } |
4962 | |
4963 | if (_selectionOverlay!.magnifierIsVisible) { |
4964 | _selectionOverlay!.updateMagnifier(positionToShow); |
4965 | } else { |
4966 | _selectionOverlay!.showMagnifier(positionToShow); |
4967 | } |
4968 | } |
4969 | |
4970 | /// Hides the magnifier if it is visible. |
4971 | void hideMagnifier() { |
4972 | if (_selectionOverlay == null) { |
4973 | return; |
4974 | } |
4975 | |
4976 | if (_selectionOverlay!.magnifierIsVisible) { |
4977 | _selectionOverlay!.hideMagnifier(); |
4978 | } |
4979 | } |
4980 | |
4981 | // Tracks the location a [_ScribblePlaceholder] should be rendered in the |
4982 | // text. |
4983 | // |
4984 | // A value of -1 indicates there should be no placeholder, otherwise the |
4985 | // value should be between 0 and the length of the text, inclusive. |
4986 | int _placeholderLocation = -1; |
4987 | |
4988 | @override |
4989 | void insertTextPlaceholder(Size size) { |
4990 | if (!_stylusHandwritingEnabled) { |
4991 | return; |
4992 | } |
4993 | |
4994 | if (!widget.controller.selection.isValid) { |
4995 | return; |
4996 | } |
4997 | |
4998 | setState(() { |
4999 | _placeholderLocation = _value.text.length - widget.controller.selection.end; |
5000 | }); |
5001 | } |
5002 | |
5003 | @override |
5004 | void removeTextPlaceholder() { |
5005 | if (!_stylusHandwritingEnabled || _placeholderLocation == -1) { |
5006 | return; |
5007 | } |
5008 | |
5009 | setState(() { |
5010 | _placeholderLocation = -1; |
5011 | }); |
5012 | } |
5013 | |
5014 | @override |
5015 | void performSelector(String selectorName) { |
5016 | final Intent? intent = intentForMacOSSelector(selectorName); |
5017 | |
5018 | if (intent != null) { |
5019 | final BuildContext? primaryContext = primaryFocus?.context; |
5020 | if (primaryContext != null) { |
5021 | Actions.invoke(primaryContext, intent); |
5022 | } |
5023 | } |
5024 | } |
5025 | |
5026 | @override |
5027 | String get autofillId => 'EditableText-$hashCode'; |
5028 | |
5029 | int? _viewId; |
5030 | |
5031 | @override |
5032 | TextInputConfiguration get textInputConfiguration { |
5033 | final List<String>? autofillHints = widget.autofillHints?.toList(growable: false); |
5034 | final AutofillConfiguration autofillConfiguration = |
5035 | autofillHints != null |
5036 | ? AutofillConfiguration( |
5037 | uniqueIdentifier: autofillId, |
5038 | autofillHints: autofillHints, |
5039 | currentEditingValue: currentTextEditingValue, |
5040 | ) |
5041 | : AutofillConfiguration.disabled; |
5042 | |
5043 | _viewId = View.of(context).viewId; |
5044 | return TextInputConfiguration( |
5045 | viewId: _viewId, |
5046 | inputType: widget.keyboardType, |
5047 | readOnly: widget.readOnly, |
5048 | obscureText: widget.obscureText, |
5049 | autocorrect: widget.autocorrect, |
5050 | smartDashesType: widget.smartDashesType, |
5051 | smartQuotesType: widget.smartQuotesType, |
5052 | enableSuggestions: widget.enableSuggestions, |
5053 | enableInteractiveSelection: widget._userSelectionEnabled, |
5054 | inputAction: |
5055 | widget.textInputAction ?? |
5056 | (widget.keyboardType == TextInputType.multiline |
5057 | ? TextInputAction.newline |
5058 | : TextInputAction.done), |
5059 | textCapitalization: widget.textCapitalization, |
5060 | keyboardAppearance: widget.keyboardAppearance, |
5061 | autofillConfiguration: autofillConfiguration, |
5062 | enableIMEPersonalizedLearning: widget.enableIMEPersonalizedLearning, |
5063 | allowedMimeTypes: |
5064 | widget.contentInsertionConfiguration == null |
5065 | ? const <String>[] |
5066 | : widget.contentInsertionConfiguration!.allowedMimeTypes, |
5067 | ); |
5068 | } |
5069 | |
5070 | @override |
5071 | void autofill(TextEditingValue value) => updateEditingValue(value); |
5072 | |
5073 | // null if no promptRect should be shown. |
5074 | TextRange? _currentPromptRectRange; |
5075 | |
5076 | @override |
5077 | void showAutocorrectionPromptRect(int start, int end) { |
5078 | setState(() { |
5079 | _currentPromptRectRange = TextRange(start: start, end: end); |
5080 | }); |
5081 | } |
5082 | |
5083 | VoidCallback? _semanticsOnCopy(TextSelectionControls? controls) { |
5084 | return widget.selectionEnabled && |
5085 | _hasFocus && |
5086 | (widget.selectionControls is TextSelectionHandleControls |
5087 | ? copyEnabled |
5088 | : copyEnabled && (widget.selectionControls?.canCopy(this) ?? false)) |
5089 | ? () { |
5090 | controls?.handleCopy(this); |
5091 | copySelection(SelectionChangedCause.toolbar); |
5092 | } |
5093 | : null; |
5094 | } |
5095 | |
5096 | VoidCallback? _semanticsOnCut(TextSelectionControls? controls) { |
5097 | return widget.selectionEnabled && |
5098 | _hasFocus && |
5099 | (widget.selectionControls is TextSelectionHandleControls |
5100 | ? cutEnabled |
5101 | : cutEnabled && (widget.selectionControls?.canCut(this) ?? false)) |
5102 | ? () { |
5103 | controls?.handleCut(this); |
5104 | cutSelection(SelectionChangedCause.toolbar); |
5105 | } |
5106 | : null; |
5107 | } |
5108 | |
5109 | VoidCallback? _semanticsOnPaste(TextSelectionControls? controls) { |
5110 | return widget.selectionEnabled && |
5111 | _hasFocus && |
5112 | (widget.selectionControls is TextSelectionHandleControls |
5113 | ? pasteEnabled |
5114 | : pasteEnabled && (widget.selectionControls?.canPaste(this) ?? false)) && |
5115 | (clipboardStatus.value == ClipboardStatus.pasteable) |
5116 | ? () { |
5117 | controls?.handlePaste(this); |
5118 | pasteText(SelectionChangedCause.toolbar); |
5119 | } |
5120 | : null; |
5121 | } |
5122 | |
5123 | // Returns the closest boundary location to `extent` but not including `extent` |
5124 | // itself (unless already at the start/end of the text), in the direction |
5125 | // specified by `forward`. |
5126 | TextPosition _moveBeyondTextBoundary( |
5127 | TextPosition extent, |
5128 | bool forward, |
5129 | TextBoundary textBoundary, |
5130 | ) { |
5131 | assert(extent.offset >= 0); |
5132 | final int newOffset = |
5133 | forward |
5134 | ? textBoundary.getTrailingTextBoundaryAt(extent.offset) ?? _value.text.length |
5135 | // if x is a boundary defined by `textBoundary`, most textBoundaries (except |
5136 | // LineBreaker) guarantees `x == textBoundary.getLeadingTextBoundaryAt(x)`. |
5137 | // Use x - 1 here to make sure we don't get stuck at the fixed point x. |
5138 | : textBoundary.getLeadingTextBoundaryAt(extent.offset - 1) ?? 0; |
5139 | return TextPosition(offset: newOffset); |
5140 | } |
5141 | |
5142 | // Returns the closest boundary location to `extent`, including `extent` |
5143 | // itself, in the direction specified by `forward`. |
5144 | // |
5145 | // This method returns a fixed point of itself: applying `_toTextBoundary` |
5146 | // again on the returned TextPosition gives the same TextPosition. It's used |
5147 | // exclusively for handling line boundaries, since performing "move to line |
5148 | // start" more than once usually doesn't move you to the previous line. |
5149 | TextPosition _moveToTextBoundary(TextPosition extent, bool forward, TextBoundary textBoundary) { |
5150 | assert(extent.offset >= 0); |
5151 | final int caretOffset; |
5152 | switch (extent.affinity) { |
5153 | case TextAffinity.upstream: |
5154 | if (extent.offset < 1 && !forward) { |
5155 | assert(extent.offset == 0); |
5156 | return const TextPosition(offset: 0); |
5157 | } |
5158 | // When the text affinity is upstream, the caret is associated with the |
5159 | // grapheme before the code unit at `extent.offset`. |
5160 | // TODO(LongCatIsLooong): don't assume extent.offset is at a grapheme |
5161 | // boundary, and do this instead: |
5162 | // final int graphemeStart = CharacterRange.at(string, extent.offset).stringBeforeLength - 1; |
5163 | caretOffset = math.max(0, extent.offset - 1); |
5164 | case TextAffinity.downstream: |
5165 | caretOffset = extent.offset; |
5166 | } |
5167 | // The line boundary range does not include some control characters |
5168 | // (most notably, Line Feed), in which case there's |
5169 | // `x ∉ getTextBoundaryAt(x)`. In case `caretOffset` points to one such |
5170 | // control character, we define that these control characters themselves are |
5171 | // still part of the previous line, but also exclude them from the |
5172 | // line boundary range since they're non-printing. IOW, no additional |
5173 | // processing needed since the LineBoundary class does exactly that. |
5174 | return forward |
5175 | ? TextPosition( |
5176 | offset: textBoundary.getTrailingTextBoundaryAt(caretOffset) ?? _value.text.length, |
5177 | affinity: TextAffinity.upstream, |
5178 | ) |
5179 | : TextPosition(offset: textBoundary.getLeadingTextBoundaryAt(caretOffset) ?? 0); |
5180 | } |
5181 | |
5182 | // --------------------------- Text Editing Actions --------------------------- |
5183 | |
5184 | TextBoundary _characterBoundary() => |
5185 | widget.obscureText ? _CodePointBoundary(_value.text) : CharacterBoundary(_value.text); |
5186 | TextBoundary _nextWordBoundary() => |
5187 | widget.obscureText ? _documentBoundary() : renderEditable.wordBoundaries.moveByWordBoundary; |
5188 | TextBoundary _linebreak() => |
5189 | widget.obscureText ? _documentBoundary() : LineBoundary(renderEditable); |
5190 | TextBoundary _paragraphBoundary() => ParagraphBoundary(_value.text); |
5191 | TextBoundary _documentBoundary() => DocumentBoundary(_value.text); |
5192 | |
5193 | Action<T> _makeOverridable<T extends Intent>(Action<T> defaultAction) { |
5194 | return Action<T>.overridable(context: context, defaultAction: defaultAction); |
5195 | } |
5196 | |
5197 | /// Transpose the characters immediately before and after the current |
5198 | /// collapsed selection. |
5199 | /// |
5200 | /// When the cursor is at the end of the text, transposes the last two |
5201 | /// characters, if they exist. |
5202 | /// |
5203 | /// When the cursor is at the start of the text, does nothing. |
5204 | void _transposeCharacters(TransposeCharactersIntent intent) { |
5205 | if (_value.text.characters.length <= 1 || |
5206 | !_value.selection.isCollapsed || |
5207 | _value.selection.baseOffset == 0) { |
5208 | return; |
5209 | } |
5210 | |
5211 | final String text = _value.text; |
5212 | final TextSelection selection = _value.selection; |
5213 | final bool atEnd = selection.baseOffset == text.length; |
5214 | final CharacterRange transposing = CharacterRange.at(text, selection.baseOffset); |
5215 | if (atEnd) { |
5216 | transposing.moveBack(2); |
5217 | } else { |
5218 | transposing |
5219 | ..moveBack() |
5220 | ..expandNext(); |
5221 | } |
5222 | assert(transposing.currentCharacters.length == 2); |
5223 | |
5224 | userUpdateTextEditingValue( |
5225 | TextEditingValue( |
5226 | text: |
5227 | transposing.stringBefore + |
5228 | transposing.currentCharacters.last + |
5229 | transposing.currentCharacters.first + |
5230 | transposing.stringAfter, |
5231 | selection: TextSelection.collapsed( |
5232 | offset: transposing.stringBeforeLength + transposing.current.length, |
5233 | ), |
5234 | ), |
5235 | SelectionChangedCause.keyboard, |
5236 | ); |
5237 | } |
5238 | |
5239 | late final Action<TransposeCharactersIntent> _transposeCharactersAction = |
5240 | CallbackAction<TransposeCharactersIntent>(onInvoke: _transposeCharacters); |
5241 | |
5242 | void _replaceText(ReplaceTextIntent intent) { |
5243 | final TextEditingValue oldValue = _value; |
5244 | final TextEditingValue newValue = intent.currentTextEditingValue.replaced( |
5245 | intent.replacementRange, |
5246 | intent.replacementText, |
5247 | ); |
5248 | userUpdateTextEditingValue(newValue, intent.cause); |
5249 | |
5250 | // If there's no change in text and selection (e.g. when selecting and |
5251 | // pasting identical text), the widget won't be rebuilt on value update. |
5252 | // Handle this by calling _didChangeTextEditingValue() so caret and scroll |
5253 | // updates can happen. |
5254 | if (newValue == oldValue) { |
5255 | _didChangeTextEditingValue(); |
5256 | } |
5257 | } |
5258 | |
5259 | late final Action<ReplaceTextIntent> _replaceTextAction = CallbackAction<ReplaceTextIntent>( |
5260 | onInvoke: _replaceText, |
5261 | ); |
5262 | |
5263 | // Scrolls either to the beginning or end of the document depending on the |
5264 | // intent's `forward` parameter. |
5265 | void _scrollToDocumentBoundary(ScrollToDocumentBoundaryIntent intent) { |
5266 | if (intent.forward) { |
5267 | bringIntoView(TextPosition(offset: _value.text.length)); |
5268 | } else { |
5269 | bringIntoView(const TextPosition(offset: 0)); |
5270 | } |
5271 | } |
5272 | |
5273 | /// Handles [ScrollIntent] by scrolling the [Scrollable] inside of |
5274 | /// [EditableText]. |
5275 | void _scroll(ScrollIntent intent) { |
5276 | if (intent.type != ScrollIncrementType.page) { |
5277 | return; |
5278 | } |
5279 | |
5280 | final ScrollPosition position = _scrollController.position; |
5281 | if (widget.maxLines == 1) { |
5282 | _scrollController.jumpTo(position.maxScrollExtent); |
5283 | return; |
5284 | } |
5285 | |
5286 | // If the field isn't scrollable, do nothing. For example, when the lines of |
5287 | // text is less than maxLines, the field has nothing to scroll. |
5288 | if (position.maxScrollExtent == 0.0 && position.minScrollExtent == 0.0) { |
5289 | return; |
5290 | } |
5291 | |
5292 | final ScrollableState? state = _scrollableKey.currentState as ScrollableState?; |
5293 | final double increment = ScrollAction.getDirectionalIncrement(state!, intent); |
5294 | final double destination = clampDouble( |
5295 | position.pixels + increment, |
5296 | position.minScrollExtent, |
5297 | position.maxScrollExtent, |
5298 | ); |
5299 | if (destination == position.pixels) { |
5300 | return; |
5301 | } |
5302 | _scrollController.jumpTo(destination); |
5303 | } |
5304 | |
5305 | /// Extend the selection down by page if the `forward` parameter is true, or |
5306 | /// up by page otherwise. |
5307 | void _extendSelectionByPage(ExtendSelectionByPageIntent intent) { |
5308 | if (widget.maxLines == 1) { |
5309 | return; |
5310 | } |
5311 | |
5312 | final TextSelection nextSelection; |
5313 | final Rect extentRect = renderEditable.getLocalRectForCaret(_value.selection.extent); |
5314 | final ScrollableState? state = _scrollableKey.currentState as ScrollableState?; |
5315 | final double increment = ScrollAction.getDirectionalIncrement( |
5316 | state!, |
5317 | ScrollIntent( |
5318 | direction: intent.forward ? AxisDirection.down : AxisDirection.up, |
5319 | type: ScrollIncrementType.page, |
5320 | ), |
5321 | ); |
5322 | final ScrollPosition position = _scrollController.position; |
5323 | if (intent.forward) { |
5324 | if (_value.selection.extentOffset >= _value.text.length) { |
5325 | return; |
5326 | } |
5327 | final Offset nextExtentOffset = Offset(extentRect.left, extentRect.top + increment); |
5328 | final double height = position.maxScrollExtent + renderEditable.size.height; |
5329 | final TextPosition nextExtent = |
5330 | nextExtentOffset.dy + position.pixels >= height |
5331 | ? TextPosition(offset: _value.text.length) |
5332 | : renderEditable.getPositionForPoint(renderEditable.localToGlobal(nextExtentOffset)); |
5333 | nextSelection = _value.selection.copyWith(extentOffset: nextExtent.offset); |
5334 | } else { |
5335 | if (_value.selection.extentOffset <= 0) { |
5336 | return; |
5337 | } |
5338 | final Offset nextExtentOffset = Offset(extentRect.left, extentRect.top + increment); |
5339 | final TextPosition nextExtent = |
5340 | nextExtentOffset.dy + position.pixels <= 0 |
5341 | ? const TextPosition(offset: 0) |
5342 | : renderEditable.getPositionForPoint(renderEditable.localToGlobal(nextExtentOffset)); |
5343 | nextSelection = _value.selection.copyWith(extentOffset: nextExtent.offset); |
5344 | } |
5345 | |
5346 | bringIntoView(nextSelection.extent); |
5347 | userUpdateTextEditingValue( |
5348 | _value.copyWith(selection: nextSelection), |
5349 | SelectionChangedCause.keyboard, |
5350 | ); |
5351 | } |
5352 | |
5353 | void _updateSelection(UpdateSelectionIntent intent) { |
5354 | assert( |
5355 | intent.newSelection.start <= intent.currentTextEditingValue.text.length, |
5356 | 'invalid selection: ${intent.newSelection}: it must not exceed the current text length ${intent.currentTextEditingValue.text.length}', |
5357 | ); |
5358 | assert( |
5359 | intent.newSelection.end <= intent.currentTextEditingValue.text.length, |
5360 | 'invalid selection: ${intent.newSelection}: it must not exceed the current text length ${intent.currentTextEditingValue.text.length}', |
5361 | ); |
5362 | |
5363 | bringIntoView(intent.newSelection.extent); |
5364 | userUpdateTextEditingValue( |
5365 | intent.currentTextEditingValue.copyWith(selection: intent.newSelection), |
5366 | intent.cause, |
5367 | ); |
5368 | } |
5369 | |
5370 | late final Action<UpdateSelectionIntent> _updateSelectionAction = |
5371 | CallbackAction<UpdateSelectionIntent>(onInvoke: _updateSelection); |
5372 | |
5373 | late final _UpdateTextSelectionVerticallyAction<DirectionalCaretMovementIntent> |
5374 | _verticalSelectionUpdateAction = |
5375 | _UpdateTextSelectionVerticallyAction<DirectionalCaretMovementIntent>(this); |
5376 | |
5377 | Object? _hideToolbarIfVisible(DismissIntent intent) { |
5378 | if (_selectionOverlay?.toolbarIsVisible ?? false) { |
5379 | hideToolbar(false); |
5380 | return null; |
5381 | } |
5382 | return Actions.invoke(context, intent); |
5383 | } |
5384 | |
5385 | void _onTapOutside(BuildContext context, PointerDownEvent event) { |
5386 | _hadFocusOnTapDown = true; |
5387 | |
5388 | if (widget.onTapOutside != null) { |
5389 | widget.onTapOutside!(event); |
5390 | } else { |
5391 | _defaultOnTapOutside(context, event); |
5392 | } |
5393 | } |
5394 | |
5395 | void _onTapUpOutside(BuildContext context, PointerUpEvent event) { |
5396 | if (!_hadFocusOnTapDown) { |
5397 | return; |
5398 | } |
5399 | |
5400 | // Reset to false so that subsequent events doesn't trigger the callback based on old information. |
5401 | _hadFocusOnTapDown = false; |
5402 | |
5403 | if (widget.onTapUpOutside != null) { |
5404 | widget.onTapUpOutside!(event); |
5405 | } else { |
5406 | _defaultOnTapUpOutside(context, event); |
5407 | } |
5408 | } |
5409 | |
5410 | /// The default behavior used if [EditableText.onTapOutside] is null. |
5411 | /// |
5412 | /// The `event` argument is the [PointerDownEvent] that caused the notification. |
5413 | void _defaultOnTapOutside(BuildContext context, PointerDownEvent event) { |
5414 | Actions.invoke( |
5415 | context, |
5416 | EditableTextTapOutsideIntent(focusNode: widget.focusNode, pointerDownEvent: event), |
5417 | ); |
5418 | } |
5419 | |
5420 | /// The default behavior used if [EditableText.onTapUpOutside] is null. |
5421 | /// |
5422 | /// The `event` argument is the [PointerUpEvent] that caused the notification. |
5423 | void _defaultOnTapUpOutside(BuildContext context, PointerUpEvent event) { |
5424 | Actions.invoke( |
5425 | context, |
5426 | EditableTextTapUpOutsideIntent(focusNode: widget.focusNode, pointerUpEvent: event), |
5427 | ); |
5428 | } |
5429 | |
5430 | late final Map<Type, Action<Intent>> _actions = <Type, Action<Intent>>{ |
5431 | DoNothingAndStopPropagationTextIntent: DoNothingAction(consumesKey: false), |
5432 | ReplaceTextIntent: _replaceTextAction, |
5433 | UpdateSelectionIntent: _updateSelectionAction, |
5434 | DirectionalFocusIntent: DirectionalFocusAction.forTextField(), |
5435 | DismissIntent: CallbackAction<DismissIntent>(onInvoke: _hideToolbarIfVisible), |
5436 | |
5437 | // Delete |
5438 | DeleteCharacterIntent: _makeOverridable( |
5439 | _DeleteTextAction<DeleteCharacterIntent>(this, _characterBoundary, _moveBeyondTextBoundary), |
5440 | ), |
5441 | DeleteToNextWordBoundaryIntent: _makeOverridable( |
5442 | _DeleteTextAction<DeleteToNextWordBoundaryIntent>( |
5443 | this, |
5444 | _nextWordBoundary, |
5445 | _moveBeyondTextBoundary, |
5446 | ), |
5447 | ), |
5448 | DeleteToLineBreakIntent: _makeOverridable( |
5449 | _DeleteTextAction<DeleteToLineBreakIntent>(this, _linebreak, _moveToTextBoundary), |
5450 | ), |
5451 | |
5452 | // Extend/Move Selection |
5453 | ExtendSelectionByCharacterIntent: _makeOverridable( |
5454 | _UpdateTextSelectionAction<ExtendSelectionByCharacterIntent>( |
5455 | this, |
5456 | _characterBoundary, |
5457 | _moveBeyondTextBoundary, |
5458 | ignoreNonCollapsedSelection: false, |
5459 | ), |
5460 | ), |
5461 | ExtendSelectionByPageIntent: _makeOverridable( |
5462 | CallbackAction<ExtendSelectionByPageIntent>(onInvoke: _extendSelectionByPage), |
5463 | ), |
5464 | ExtendSelectionToNextWordBoundaryIntent: _makeOverridable( |
5465 | _UpdateTextSelectionAction<ExtendSelectionToNextWordBoundaryIntent>( |
5466 | this, |
5467 | _nextWordBoundary, |
5468 | _moveBeyondTextBoundary, |
5469 | ignoreNonCollapsedSelection: true, |
5470 | ), |
5471 | ), |
5472 | ExtendSelectionToNextParagraphBoundaryIntent: _makeOverridable( |
5473 | _UpdateTextSelectionAction<ExtendSelectionToNextParagraphBoundaryIntent>( |
5474 | this, |
5475 | _paragraphBoundary, |
5476 | _moveBeyondTextBoundary, |
5477 | ignoreNonCollapsedSelection: true, |
5478 | ), |
5479 | ), |
5480 | ExtendSelectionToLineBreakIntent: _makeOverridable( |
5481 | _UpdateTextSelectionAction<ExtendSelectionToLineBreakIntent>( |
5482 | this, |
5483 | _linebreak, |
5484 | _moveToTextBoundary, |
5485 | ignoreNonCollapsedSelection: true, |
5486 | ), |
5487 | ), |
5488 | ExtendSelectionVerticallyToAdjacentLineIntent: _makeOverridable(_verticalSelectionUpdateAction), |
5489 | ExtendSelectionVerticallyToAdjacentPageIntent: _makeOverridable(_verticalSelectionUpdateAction), |
5490 | ExtendSelectionToNextParagraphBoundaryOrCaretLocationIntent: _makeOverridable( |
5491 | _UpdateTextSelectionAction<ExtendSelectionToNextParagraphBoundaryOrCaretLocationIntent>( |
5492 | this, |
5493 | _paragraphBoundary, |
5494 | _moveBeyondTextBoundary, |
5495 | ignoreNonCollapsedSelection: true, |
5496 | ), |
5497 | ), |
5498 | ExtendSelectionToDocumentBoundaryIntent: _makeOverridable( |
5499 | _UpdateTextSelectionAction<ExtendSelectionToDocumentBoundaryIntent>( |
5500 | this, |
5501 | _documentBoundary, |
5502 | _moveBeyondTextBoundary, |
5503 | ignoreNonCollapsedSelection: true, |
5504 | ), |
5505 | ), |
5506 | ExtendSelectionToNextWordBoundaryOrCaretLocationIntent: _makeOverridable( |
5507 | _UpdateTextSelectionAction<ExtendSelectionToNextWordBoundaryOrCaretLocationIntent>( |
5508 | this, |
5509 | _nextWordBoundary, |
5510 | _moveBeyondTextBoundary, |
5511 | ignoreNonCollapsedSelection: true, |
5512 | ), |
5513 | ), |
5514 | ScrollToDocumentBoundaryIntent: _makeOverridable( |
5515 | CallbackAction<ScrollToDocumentBoundaryIntent>(onInvoke: _scrollToDocumentBoundary), |
5516 | ), |
5517 | ScrollIntent: CallbackAction<ScrollIntent>(onInvoke: _scroll), |
5518 | |
5519 | // Expand Selection |
5520 | ExpandSelectionToLineBreakIntent: _makeOverridable( |
5521 | _UpdateTextSelectionAction<ExpandSelectionToLineBreakIntent>( |
5522 | this, |
5523 | _linebreak, |
5524 | _moveToTextBoundary, |
5525 | ignoreNonCollapsedSelection: true, |
5526 | isExpand: true, |
5527 | ), |
5528 | ), |
5529 | ExpandSelectionToDocumentBoundaryIntent: _makeOverridable( |
5530 | _UpdateTextSelectionAction<ExpandSelectionToDocumentBoundaryIntent>( |
5531 | this, |
5532 | _documentBoundary, |
5533 | _moveToTextBoundary, |
5534 | ignoreNonCollapsedSelection: true, |
5535 | isExpand: true, |
5536 | extentAtIndex: true, |
5537 | ), |
5538 | ), |
5539 | |
5540 | // Copy Paste |
5541 | SelectAllTextIntent: _makeOverridable(_SelectAllAction(this)), |
5542 | CopySelectionTextIntent: _makeOverridable(_CopySelectionAction(this)), |
5543 | PasteTextIntent: _makeOverridable( |
5544 | CallbackAction<PasteTextIntent>( |
5545 | onInvoke: (PasteTextIntent intent) => pasteText(intent.cause), |
5546 | ), |
5547 | ), |
5548 | |
5549 | TransposeCharactersIntent: _makeOverridable(_transposeCharactersAction), |
5550 | EditableTextTapOutsideIntent: _makeOverridable(_EditableTextTapOutsideAction()), |
5551 | EditableTextTapUpOutsideIntent: _makeOverridable(_EditableTextTapUpOutsideAction()), |
5552 | }; |
5553 | |
5554 | @protected |
5555 | @override |
5556 | Widget build(BuildContext context) { |
5557 | assert(debugCheckHasMediaQuery(context)); |
5558 | super.build(context); // See AutomaticKeepAliveClientMixin. |
5559 | |
5560 | final TextSelectionControls? controls = widget.selectionControls; |
5561 | final TextScaler effectiveTextScaler = switch ((widget.textScaler, widget.textScaleFactor)) { |
5562 | (final TextScaler textScaler, _) => textScaler, |
5563 | (null, final double textScaleFactor) => TextScaler.linear(textScaleFactor), |
5564 | (null, null) => MediaQuery.textScalerOf(context), |
5565 | }; |
5566 | final ui.SemanticsInputType inputType; |
5567 | switch (widget.keyboardType) { |
5568 | case TextInputType.phone: |
5569 | inputType = ui.SemanticsInputType.phone; |
5570 | case TextInputType.url: |
5571 | inputType = ui.SemanticsInputType.url; |
5572 | case TextInputType.emailAddress: |
5573 | inputType = ui.SemanticsInputType.email; |
5574 | default: |
5575 | inputType = ui.SemanticsInputType.text; |
5576 | } |
5577 | |
5578 | return _CompositionCallback( |
5579 | compositeCallback: _compositeCallback, |
5580 | enabled: _hasInputConnection, |
5581 | child: Actions( |
5582 | actions: _actions, |
5583 | child: Builder( |
5584 | builder: (BuildContext context) { |
5585 | return TextFieldTapRegion( |
5586 | groupId: widget.groupId, |
5587 | onTapOutside: |
5588 | _hasFocus ? (PointerDownEvent event) => _onTapOutside(context, event) : null, |
5589 | onTapUpOutside: (PointerUpEvent event) => _onTapUpOutside(context, event), |
5590 | debugLabel: kReleaseMode ? null : 'EditableText', |
5591 | child: MouseRegion( |
5592 | cursor: widget.mouseCursor ?? SystemMouseCursors.text, |
5593 | child: UndoHistory<TextEditingValue>( |
5594 | value: widget.controller, |
5595 | onTriggered: (TextEditingValue value) { |
5596 | userUpdateTextEditingValue(value, SelectionChangedCause.keyboard); |
5597 | }, |
5598 | shouldChangeUndoStack: (TextEditingValue? oldValue, TextEditingValue newValue) { |
5599 | if (!newValue.selection.isValid) { |
5600 | return false; |
5601 | } |
5602 | |
5603 | if (oldValue == null) { |
5604 | return true; |
5605 | } |
5606 | |
5607 | switch (defaultTargetPlatform) { |
5608 | case TargetPlatform.iOS: |
5609 | case TargetPlatform.macOS: |
5610 | case TargetPlatform.fuchsia: |
5611 | case TargetPlatform.linux: |
5612 | case TargetPlatform.windows: |
5613 | // Composing text is not counted in history coalescing. |
5614 | if (!widget.controller.value.composing.isCollapsed) { |
5615 | return false; |
5616 | } |
5617 | case TargetPlatform.android: |
5618 | // Gboard on Android puts non-CJK words in composing regions. Coalesce |
5619 | // composing text in order to allow the saving of partial words in that |
5620 | // case. |
5621 | break; |
5622 | } |
5623 | |
5624 | return oldValue.text != newValue.text || |
5625 | oldValue.composing != newValue.composing; |
5626 | }, |
5627 | undoStackModifier: (TextEditingValue value) { |
5628 | // On Android we should discard the composing region when pushing |
5629 | // a new entry to the undo stack. This prevents the TextInputPlugin |
5630 | // from restarting the input on every undo/redo when the composing |
5631 | // region is changed by the framework. |
5632 | return defaultTargetPlatform == TargetPlatform.android |
5633 | ? value.copyWith(composing: TextRange.empty) |
5634 | : value; |
5635 | }, |
5636 | focusNode: widget.focusNode, |
5637 | controller: widget.undoController, |
5638 | child: Focus( |
5639 | focusNode: widget.focusNode, |
5640 | includeSemantics: false, |
5641 | debugLabel: kReleaseMode ? null : 'EditableText', |
5642 | child: NotificationListener<ScrollNotification>( |
5643 | onNotification: (ScrollNotification notification) { |
5644 | _handleContextMenuOnScroll(notification); |
5645 | _scribbleCacheKey = null; |
5646 | return false; |
5647 | }, |
5648 | child: Scrollable( |
5649 | key: _scrollableKey, |
5650 | excludeFromSemantics: true, |
5651 | axisDirection: _isMultiline ? AxisDirection.down : AxisDirection.right, |
5652 | controller: _scrollController, |
5653 | physics: widget.scrollPhysics, |
5654 | dragStartBehavior: widget.dragStartBehavior, |
5655 | restorationId: widget.restorationId, |
5656 | // If a ScrollBehavior is not provided, only apply scrollbars when |
5657 | // multiline. The overscroll indicator should not be applied in |
5658 | // either case, glowing or stretching. |
5659 | scrollBehavior: |
5660 | widget.scrollBehavior ?? |
5661 | ScrollConfiguration.of( |
5662 | context, |
5663 | ).copyWith(scrollbars: _isMultiline, overscroll: false), |
5664 | viewportBuilder: (BuildContext context, ViewportOffset offset) { |
5665 | return CompositedTransformTarget( |
5666 | link: _toolbarLayerLink, |
5667 | child: Semantics( |
5668 | inputType: inputType, |
5669 | onCopy: _semanticsOnCopy(controls), |
5670 | onCut: _semanticsOnCut(controls), |
5671 | onPaste: _semanticsOnPaste(controls), |
5672 | child: _ScribbleFocusable( |
5673 | editableKey: _editableKey, |
5674 | enabled: _stylusHandwritingEnabled, |
5675 | focusNode: widget.focusNode, |
5676 | updateSelectionRects: () { |
5677 | _openInputConnection(); |
5678 | _updateSelectionRects(force: true); |
5679 | }, |
5680 | child: SizeChangedLayoutNotifier( |
5681 | child: _Editable( |
5682 | key: _editableKey, |
5683 | startHandleLayerLink: _startHandleLayerLink, |
5684 | endHandleLayerLink: _endHandleLayerLink, |
5685 | inlineSpan: buildTextSpan(), |
5686 | value: _value, |
5687 | cursorColor: _cursorColor, |
5688 | backgroundCursorColor: widget.backgroundCursorColor, |
5689 | showCursor: _cursorVisibilityNotifier, |
5690 | forceLine: widget.forceLine, |
5691 | readOnly: widget.readOnly, |
5692 | hasFocus: _hasFocus, |
5693 | maxLines: widget.maxLines, |
5694 | minLines: widget.minLines, |
5695 | expands: widget.expands, |
5696 | strutStyle: widget.strutStyle, |
5697 | selectionColor: |
5698 | _selectionOverlay?.spellCheckToolbarIsVisible ?? false |
5699 | ? _spellCheckConfiguration.misspelledSelectionColor ?? |
5700 | widget.selectionColor |
5701 | : widget.selectionColor, |
5702 | textScaler: effectiveTextScaler, |
5703 | textAlign: widget.textAlign, |
5704 | textDirection: _textDirection, |
5705 | locale: widget.locale, |
5706 | textHeightBehavior: |
5707 | widget.textHeightBehavior ?? |
5708 | DefaultTextHeightBehavior.maybeOf(context), |
5709 | textWidthBasis: widget.textWidthBasis, |
5710 | obscuringCharacter: widget.obscuringCharacter, |
5711 | obscureText: widget.obscureText, |
5712 | offset: offset, |
5713 | rendererIgnoresPointer: widget.rendererIgnoresPointer, |
5714 | cursorWidth: widget.cursorWidth, |
5715 | cursorHeight: widget.cursorHeight, |
5716 | cursorRadius: widget.cursorRadius, |
5717 | cursorOffset: widget.cursorOffset ?? Offset.zero, |
5718 | selectionHeightStyle: widget.selectionHeightStyle, |
5719 | selectionWidthStyle: widget.selectionWidthStyle, |
5720 | paintCursorAboveText: widget.paintCursorAboveText, |
5721 | enableInteractiveSelection: widget._userSelectionEnabled, |
5722 | textSelectionDelegate: this, |
5723 | devicePixelRatio: _devicePixelRatio, |
5724 | promptRectRange: _currentPromptRectRange, |
5725 | promptRectColor: widget.autocorrectionTextRectColor, |
5726 | clipBehavior: widget.clipBehavior, |
5727 | ), |
5728 | ), |
5729 | ), |
5730 | ), |
5731 | ); |
5732 | }, |
5733 | ), |
5734 | ), |
5735 | ), |
5736 | ), |
5737 | ), |
5738 | ); |
5739 | }, |
5740 | ), |
5741 | ), |
5742 | ); |
5743 | } |
5744 | |
5745 | /// Builds [TextSpan] from current editing value. |
5746 | /// |
5747 | /// By default makes text in composing range appear as underlined. |
5748 | /// Descendants can override this method to customize appearance of text. |
5749 | TextSpan buildTextSpan() { |
5750 | if (widget.obscureText) { |
5751 | String text = _value.text; |
5752 | text = widget.obscuringCharacter * text.length; |
5753 | // Reveal the latest character in an obscured field only on mobile. |
5754 | const Set<TargetPlatform> mobilePlatforms = <TargetPlatform>{ |
5755 | TargetPlatform.android, |
5756 | TargetPlatform.fuchsia, |
5757 | TargetPlatform.iOS, |
5758 | }; |
5759 | final bool brieflyShowPassword = |
5760 | WidgetsBinding.instance.platformDispatcher.brieflyShowPassword && |
5761 | mobilePlatforms.contains(defaultTargetPlatform); |
5762 | if (brieflyShowPassword) { |
5763 | final int? o = _obscureShowCharTicksPending > 0 ? _obscureLatestCharIndex : null; |
5764 | if (o != null && o >= 0 && o < text.length) { |
5765 | text = text.replaceRange(o, o + 1, _value.text.substring(o, o + 1)); |
5766 | } |
5767 | } |
5768 | return TextSpan(style: _style, text: text); |
5769 | } |
5770 | if (_placeholderLocation >= 0 && _placeholderLocation <= _value.text.length) { |
5771 | final List<_ScribblePlaceholder> placeholders = <_ScribblePlaceholder>[]; |
5772 | final int placeholderLocation = _value.text.length - _placeholderLocation; |
5773 | if (_isMultiline) { |
5774 | // The zero size placeholder here allows the line to break and keep the caret on the first line. |
5775 | placeholders.add(const _ScribblePlaceholder(child: SizedBox.shrink(), size: Size.zero)); |
5776 | placeholders.add( |
5777 | _ScribblePlaceholder( |
5778 | child: const SizedBox.shrink(), |
5779 | size: Size(renderEditable.size.width, 0.0), |
5780 | ), |
5781 | ); |
5782 | } else { |
5783 | placeholders.add( |
5784 | const _ScribblePlaceholder(child: SizedBox.shrink(), size: Size(100.0, 0.0)), |
5785 | ); |
5786 | } |
5787 | return TextSpan( |
5788 | style: _style, |
5789 | children: <InlineSpan>[ |
5790 | TextSpan(text: _value.text.substring(0, placeholderLocation)), |
5791 | ...placeholders, |
5792 | TextSpan(text: _value.text.substring(placeholderLocation)), |
5793 | ], |
5794 | ); |
5795 | } |
5796 | final bool withComposing = !widget.readOnly && _hasFocus; |
5797 | if (_spellCheckResultsReceived) { |
5798 | // If the composing range is out of range for the current text, ignore it to |
5799 | // preserve the tree integrity, otherwise in release mode a RangeError will |
5800 | // be thrown and this EditableText will be built with a broken subtree. |
5801 | assert(!_value.composing.isValid || !withComposing || _value.isComposingRangeValid); |
5802 | |
5803 | final bool composingRegionOutOfRange = !_value.isComposingRangeValid || !withComposing; |
5804 | |
5805 | return buildTextSpanWithSpellCheckSuggestions( |
5806 | _value, |
5807 | composingRegionOutOfRange, |
5808 | _style, |
5809 | _spellCheckConfiguration.misspelledTextStyle!, |
5810 | spellCheckResults!, |
5811 | ); |
5812 | } |
5813 | |
5814 | // Read only mode should not paint text composing. |
5815 | return widget.controller.buildTextSpan( |
5816 | context: context, |
5817 | style: _style, |
5818 | withComposing: withComposing, |
5819 | ); |
5820 | } |
5821 | } |
5822 | |
5823 | class _Editable extends MultiChildRenderObjectWidget { |
5824 | _Editable({ |
5825 | super.key, |
5826 | required this.inlineSpan, |
5827 | required this.value, |
5828 | required this.startHandleLayerLink, |
5829 | required this.endHandleLayerLink, |
5830 | this.cursorColor, |
5831 | this.backgroundCursorColor, |
5832 | required this.showCursor, |
5833 | required this.forceLine, |
5834 | required this.readOnly, |
5835 | this.textHeightBehavior, |
5836 | required this.textWidthBasis, |
5837 | required this.hasFocus, |
5838 | required this.maxLines, |
5839 | this.minLines, |
5840 | required this.expands, |
5841 | this.strutStyle, |
5842 | this.selectionColor, |
5843 | required this.textScaler, |
5844 | required this.textAlign, |
5845 | required this.textDirection, |
5846 | this.locale, |
5847 | required this.obscuringCharacter, |
5848 | required this.obscureText, |
5849 | required this.offset, |
5850 | this.rendererIgnoresPointer = false, |
5851 | required this.cursorWidth, |
5852 | this.cursorHeight, |
5853 | this.cursorRadius, |
5854 | required this.cursorOffset, |
5855 | required this.paintCursorAboveText, |
5856 | this.selectionHeightStyle = ui.BoxHeightStyle.tight, |
5857 | this.selectionWidthStyle = ui.BoxWidthStyle.tight, |
5858 | this.enableInteractiveSelection = true, |
5859 | required this.textSelectionDelegate, |
5860 | required this.devicePixelRatio, |
5861 | this.promptRectRange, |
5862 | this.promptRectColor, |
5863 | required this.clipBehavior, |
5864 | }) : super(children: WidgetSpan.extractFromInlineSpan(inlineSpan, textScaler)); |
5865 | |
5866 | final InlineSpan inlineSpan; |
5867 | final TextEditingValue value; |
5868 | final Color? cursorColor; |
5869 | final LayerLink startHandleLayerLink; |
5870 | final LayerLink endHandleLayerLink; |
5871 | final Color? backgroundCursorColor; |
5872 | final ValueNotifier<bool> showCursor; |
5873 | final bool forceLine; |
5874 | final bool readOnly; |
5875 | final bool hasFocus; |
5876 | final int? maxLines; |
5877 | final int? minLines; |
5878 | final bool expands; |
5879 | final StrutStyle? strutStyle; |
5880 | final Color? selectionColor; |
5881 | final TextScaler textScaler; |
5882 | final TextAlign textAlign; |
5883 | final TextDirection textDirection; |
5884 | final Locale? locale; |
5885 | final String obscuringCharacter; |
5886 | final bool obscureText; |
5887 | final TextHeightBehavior? textHeightBehavior; |
5888 | final TextWidthBasis textWidthBasis; |
5889 | final ViewportOffset offset; |
5890 | final bool rendererIgnoresPointer; |
5891 | final double cursorWidth; |
5892 | final double? cursorHeight; |
5893 | final Radius? cursorRadius; |
5894 | final Offset cursorOffset; |
5895 | final bool paintCursorAboveText; |
5896 | final ui.BoxHeightStyle selectionHeightStyle; |
5897 | final ui.BoxWidthStyle selectionWidthStyle; |
5898 | final bool enableInteractiveSelection; |
5899 | final TextSelectionDelegate textSelectionDelegate; |
5900 | final double devicePixelRatio; |
5901 | final TextRange? promptRectRange; |
5902 | final Color? promptRectColor; |
5903 | final Clip clipBehavior; |
5904 | |
5905 | @override |
5906 | RenderEditable createRenderObject(BuildContext context) { |
5907 | return RenderEditable( |
5908 | text: inlineSpan, |
5909 | cursorColor: cursorColor, |
5910 | startHandleLayerLink: startHandleLayerLink, |
5911 | endHandleLayerLink: endHandleLayerLink, |
5912 | backgroundCursorColor: backgroundCursorColor, |
5913 | showCursor: showCursor, |
5914 | forceLine: forceLine, |
5915 | readOnly: readOnly, |
5916 | hasFocus: hasFocus, |
5917 | maxLines: maxLines, |
5918 | minLines: minLines, |
5919 | expands: expands, |
5920 | strutStyle: strutStyle, |
5921 | selectionColor: selectionColor, |
5922 | textScaler: textScaler, |
5923 | textAlign: textAlign, |
5924 | textDirection: textDirection, |
5925 | locale: locale ?? Localizations.maybeLocaleOf(context), |
5926 | selection: value.selection, |
5927 | offset: offset, |
5928 | ignorePointer: rendererIgnoresPointer, |
5929 | obscuringCharacter: obscuringCharacter, |
5930 | obscureText: obscureText, |
5931 | textHeightBehavior: textHeightBehavior, |
5932 | textWidthBasis: textWidthBasis, |
5933 | cursorWidth: cursorWidth, |
5934 | cursorHeight: cursorHeight, |
5935 | cursorRadius: cursorRadius, |
5936 | cursorOffset: cursorOffset, |
5937 | paintCursorAboveText: paintCursorAboveText, |
5938 | selectionHeightStyle: selectionHeightStyle, |
5939 | selectionWidthStyle: selectionWidthStyle, |
5940 | enableInteractiveSelection: enableInteractiveSelection, |
5941 | textSelectionDelegate: textSelectionDelegate, |
5942 | devicePixelRatio: devicePixelRatio, |
5943 | promptRectRange: promptRectRange, |
5944 | promptRectColor: promptRectColor, |
5945 | clipBehavior: clipBehavior, |
5946 | ); |
5947 | } |
5948 | |
5949 | @override |
5950 | void updateRenderObject(BuildContext context, RenderEditable renderObject) { |
5951 | renderObject |
5952 | ..text = inlineSpan |
5953 | ..cursorColor = cursorColor |
5954 | ..startHandleLayerLink = startHandleLayerLink |
5955 | ..endHandleLayerLink = endHandleLayerLink |
5956 | ..backgroundCursorColor = backgroundCursorColor |
5957 | ..showCursor = showCursor |
5958 | ..forceLine = forceLine |
5959 | ..readOnly = readOnly |
5960 | ..hasFocus = hasFocus |
5961 | ..maxLines = maxLines |
5962 | ..minLines = minLines |
5963 | ..expands = expands |
5964 | ..strutStyle = strutStyle |
5965 | ..selectionColor = selectionColor |
5966 | ..textScaler = textScaler |
5967 | ..textAlign = textAlign |
5968 | ..textDirection = textDirection |
5969 | ..locale = locale ?? Localizations.maybeLocaleOf(context) |
5970 | ..selection = value.selection |
5971 | ..offset = offset |
5972 | ..ignorePointer = rendererIgnoresPointer |
5973 | ..textHeightBehavior = textHeightBehavior |
5974 | ..textWidthBasis = textWidthBasis |
5975 | ..obscuringCharacter = obscuringCharacter |
5976 | ..obscureText = obscureText |
5977 | ..cursorWidth = cursorWidth |
5978 | ..cursorHeight = cursorHeight |
5979 | ..cursorRadius = cursorRadius |
5980 | ..cursorOffset = cursorOffset |
5981 | ..selectionHeightStyle = selectionHeightStyle |
5982 | ..selectionWidthStyle = selectionWidthStyle |
5983 | ..enableInteractiveSelection = enableInteractiveSelection |
5984 | ..textSelectionDelegate = textSelectionDelegate |
5985 | ..devicePixelRatio = devicePixelRatio |
5986 | ..paintCursorAboveText = paintCursorAboveText |
5987 | ..promptRectColor = promptRectColor |
5988 | ..clipBehavior = clipBehavior |
5989 | ..setPromptRectRange(promptRectRange); |
5990 | } |
5991 | } |
5992 | |
5993 | @immutable |
5994 | class _ScribbleCacheKey { |
5995 | const _ScribbleCacheKey({ |
5996 | required this.inlineSpan, |
5997 | required this.textAlign, |
5998 | required this.textDirection, |
5999 | required this.textScaler, |
6000 | required this.textHeightBehavior, |
6001 | required this.locale, |
6002 | required this.structStyle, |
6003 | required this.placeholder, |
6004 | required this.size, |
6005 | }); |
6006 | |
6007 | final TextAlign textAlign; |
6008 | final TextDirection textDirection; |
6009 | final TextScaler textScaler; |
6010 | final TextHeightBehavior? textHeightBehavior; |
6011 | final Locale? locale; |
6012 | final StrutStyle structStyle; |
6013 | final int placeholder; |
6014 | final Size size; |
6015 | final InlineSpan inlineSpan; |
6016 | |
6017 | RenderComparison compare(_ScribbleCacheKey other) { |
6018 | if (identical(other, this)) { |
6019 | return RenderComparison.identical; |
6020 | } |
6021 | final bool needsLayout = |
6022 | textAlign != other.textAlign || |
6023 | textDirection != other.textDirection || |
6024 | textScaler != other.textScaler || |
6025 | (textHeightBehavior ?? const TextHeightBehavior()) != |
6026 | (other.textHeightBehavior ?? const TextHeightBehavior()) || |
6027 | locale != other.locale || |
6028 | structStyle != other.structStyle || |
6029 | placeholder != other.placeholder || |
6030 | size != other.size; |
6031 | return needsLayout ? RenderComparison.layout : inlineSpan.compareTo(other.inlineSpan); |
6032 | } |
6033 | } |
6034 | |
6035 | class _ScribbleFocusable extends StatefulWidget { |
6036 | const _ScribbleFocusable({ |
6037 | required this.child, |
6038 | required this.focusNode, |
6039 | required this.editableKey, |
6040 | required this.updateSelectionRects, |
6041 | required this.enabled, |
6042 | }); |
6043 | |
6044 | final Widget child; |
6045 | final FocusNode focusNode; |
6046 | final GlobalKey editableKey; |
6047 | final VoidCallback updateSelectionRects; |
6048 | final bool enabled; |
6049 | |
6050 | @override |
6051 | _ScribbleFocusableState createState() => _ScribbleFocusableState(); |
6052 | } |
6053 | |
6054 | class _ScribbleFocusableState extends State<_ScribbleFocusable> implements ScribbleClient { |
6055 | _ScribbleFocusableState() : _elementIdentifier = (_nextElementIdentifier++).toString(); |
6056 | |
6057 | @override |
6058 | void initState() { |
6059 | super.initState(); |
6060 | if (widget.enabled) { |
6061 | TextInput.registerScribbleElement(elementIdentifier, this); |
6062 | } |
6063 | } |
6064 | |
6065 | @override |
6066 | void didUpdateWidget(_ScribbleFocusable oldWidget) { |
6067 | super.didUpdateWidget(oldWidget); |
6068 | if (!oldWidget.enabled && widget.enabled) { |
6069 | TextInput.registerScribbleElement(elementIdentifier, this); |
6070 | } |
6071 | |
6072 | if (oldWidget.enabled && !widget.enabled) { |
6073 | TextInput.unregisterScribbleElement(elementIdentifier); |
6074 | } |
6075 | } |
6076 | |
6077 | @override |
6078 | void dispose() { |
6079 | TextInput.unregisterScribbleElement(elementIdentifier); |
6080 | super.dispose(); |
6081 | } |
6082 | |
6083 | RenderEditable? get renderEditable => |
6084 | widget.editableKey.currentContext?.findRenderObject() as RenderEditable?; |
6085 | |
6086 | static int _nextElementIdentifier = 1; |
6087 | final String _elementIdentifier; |
6088 | |
6089 | @override |
6090 | String get elementIdentifier => _elementIdentifier; |
6091 | |
6092 | @override |
6093 | void onScribbleFocus(Offset offset) { |
6094 | widget.focusNode.requestFocus(); |
6095 | renderEditable?.selectPositionAt(from: offset, cause: SelectionChangedCause.stylusHandwriting); |
6096 | widget.updateSelectionRects(); |
6097 | } |
6098 | |
6099 | @override |
6100 | bool isInScribbleRect(Rect rect) { |
6101 | final Rect calculatedBounds = bounds; |
6102 | if (renderEditable?.readOnly ?? false) { |
6103 | return false; |
6104 | } |
6105 | if (calculatedBounds == Rect.zero) { |
6106 | return false; |
6107 | } |
6108 | if (!calculatedBounds.overlaps(rect)) { |
6109 | return false; |
6110 | } |
6111 | final Rect intersection = calculatedBounds.intersect(rect); |
6112 | final HitTestResult result = HitTestResult(); |
6113 | WidgetsBinding.instance.hitTestInView(result, intersection.center, View.of(context).viewId); |
6114 | return result.path.any((HitTestEntry entry) => entry.target == renderEditable); |
6115 | } |
6116 | |
6117 | @override |
6118 | Rect get bounds { |
6119 | final RenderBox? box = context.findRenderObject() as RenderBox?; |
6120 | if (box == null || !mounted || !box.attached) { |
6121 | return Rect.zero; |
6122 | } |
6123 | final Matrix4 transform = box.getTransformTo(null); |
6124 | return MatrixUtils.transformRect( |
6125 | transform, |
6126 | Rect.fromLTWH(0, 0, box.size.width, box.size.height), |
6127 | ); |
6128 | } |
6129 | |
6130 | @override |
6131 | Widget build(BuildContext context) { |
6132 | return widget.child; |
6133 | } |
6134 | } |
6135 | |
6136 | class _ScribblePlaceholder extends WidgetSpan { |
6137 | const _ScribblePlaceholder({required super.child, required this.size}); |
6138 | |
6139 | /// The size of the span, used in place of adding a placeholder size to the [TextPainter]. |
6140 | final Size size; |
6141 | |
6142 | @override |
6143 | void build( |
6144 | ui.ParagraphBuilder builder, { |
6145 | TextScaler textScaler = TextScaler.noScaling, |
6146 | List<PlaceholderDimensions>? dimensions, |
6147 | }) { |
6148 | assert(debugAssertIsValid()); |
6149 | final bool hasStyle = style != null; |
6150 | if (hasStyle) { |
6151 | builder.pushStyle(style!.getTextStyle(textScaler: textScaler)); |
6152 | } |
6153 | builder.addPlaceholder(size.width, size.height, alignment); |
6154 | if (hasStyle) { |
6155 | builder.pop(); |
6156 | } |
6157 | } |
6158 | } |
6159 | |
6160 | /// A text boundary that uses code points as logical boundaries. |
6161 | /// |
6162 | /// A code point represents a single character. This may be smaller than what is |
6163 | /// represented by a user-perceived character, or grapheme. For example, a |
6164 | /// single grapheme (in this case a Unicode extended grapheme cluster) like |
6165 | /// "👨👩👦" consists of five code points: the man emoji, a zero |
6166 | /// width joiner, the woman emoji, another zero width joiner, and the boy emoji. |
6167 | /// The [String] has a length of eight because each emoji consists of two code |
6168 | /// units. |
6169 | /// |
6170 | /// Code units are the units by which Dart's String class is measured, which is |
6171 | /// encoded in UTF-16. |
6172 | /// |
6173 | /// See also: |
6174 | /// |
6175 | /// * [String.runes], which deals with code points like this class. |
6176 | /// * [Characters], which deals with graphemes. |
6177 | /// * [CharacterBoundary], which is a [TextBoundary] like this class, but whose |
6178 | /// boundaries are graphemes instead of code points. |
6179 | class _CodePointBoundary extends TextBoundary { |
6180 | const _CodePointBoundary(this._text); |
6181 | |
6182 | final String _text; |
6183 | |
6184 | // Returns true if the given position falls in the center of a surrogate pair. |
6185 | bool _breaksSurrogatePair(int position) { |
6186 | assert(position > 0 && position < _text.length && _text.length > 1); |
6187 | return TextPainter.isHighSurrogate(_text.codeUnitAt(position - 1)) && |
6188 | TextPainter.isLowSurrogate(_text.codeUnitAt(position)); |
6189 | } |
6190 | |
6191 | @override |
6192 | int? getLeadingTextBoundaryAt(int position) { |
6193 | if (_text.isEmpty || position < 0) { |
6194 | return null; |
6195 | } |
6196 | if (position == 0) { |
6197 | return 0; |
6198 | } |
6199 | if (position >= _text.length) { |
6200 | return _text.length; |
6201 | } |
6202 | if (_text.length <= 1) { |
6203 | return position; |
6204 | } |
6205 | |
6206 | return _breaksSurrogatePair(position) ? position - 1 : position; |
6207 | } |
6208 | |
6209 | @override |
6210 | int? getTrailingTextBoundaryAt(int position) { |
6211 | if (_text.isEmpty || position >= _text.length) { |
6212 | return null; |
6213 | } |
6214 | if (position < 0) { |
6215 | return 0; |
6216 | } |
6217 | if (position == _text.length - 1) { |
6218 | return _text.length; |
6219 | } |
6220 | if (_text.length <= 1) { |
6221 | return position; |
6222 | } |
6223 | |
6224 | return _breaksSurrogatePair(position + 1) ? position + 2 : position + 1; |
6225 | } |
6226 | } |
6227 | |
6228 | // ------------------------------- Text Actions ------------------------------- |
6229 | class _DeleteTextAction<T extends DirectionalTextEditingIntent> extends ContextAction<T> { |
6230 | _DeleteTextAction(this.state, this.getTextBoundary, this._applyTextBoundary); |
6231 | |
6232 | final EditableTextState state; |
6233 | final TextBoundary Function() getTextBoundary; |
6234 | final _ApplyTextBoundary _applyTextBoundary; |
6235 | |
6236 | void _hideToolbarIfTextChanged(ReplaceTextIntent intent) { |
6237 | if (state._selectionOverlay == null || !state.selectionOverlay!.toolbarIsVisible) { |
6238 | return; |
6239 | } |
6240 | final TextEditingValue oldValue = intent.currentTextEditingValue; |
6241 | final TextEditingValue newValue = intent.currentTextEditingValue.replaced( |
6242 | intent.replacementRange, |
6243 | intent.replacementText, |
6244 | ); |
6245 | if (oldValue.text != newValue.text) { |
6246 | // Hide the toolbar if the text was changed, but only hide the toolbar |
6247 | // overlay; the selection handle's visibility will be handled |
6248 | // by `_handleSelectionChanged`. |
6249 | state.hideToolbar(false); |
6250 | } |
6251 | } |
6252 | |
6253 | @override |
6254 | Object? invoke(T intent, [BuildContext? context]) { |
6255 | final TextSelection selection = state._value.selection; |
6256 | if (!selection.isValid) { |
6257 | return null; |
6258 | } |
6259 | assert(selection.isValid); |
6260 | // Expands the selection to ensure the range covers full graphemes. |
6261 | final TextBoundary atomicBoundary = state._characterBoundary(); |
6262 | if (!selection.isCollapsed) { |
6263 | // Expands the selection to ensure the range covers full graphemes. |
6264 | final TextRange range = TextRange( |
6265 | start: atomicBoundary.getLeadingTextBoundaryAt(selection.start) ?? state._value.text.length, |
6266 | end: atomicBoundary.getTrailingTextBoundaryAt(selection.end - 1) ?? 0, |
6267 | ); |
6268 | final ReplaceTextIntent replaceTextIntent = ReplaceTextIntent( |
6269 | state._value, |
6270 | '', |
6271 | range, |
6272 | SelectionChangedCause.keyboard, |
6273 | ); |
6274 | _hideToolbarIfTextChanged(replaceTextIntent); |
6275 | return Actions.invoke(context!, replaceTextIntent); |
6276 | } |
6277 | |
6278 | final int target = _applyTextBoundary(selection.base, intent.forward, getTextBoundary()).offset; |
6279 | |
6280 | final TextRange rangeToDelete = TextSelection( |
6281 | baseOffset: |
6282 | intent.forward |
6283 | ? atomicBoundary.getLeadingTextBoundaryAt(selection.baseOffset) ?? |
6284 | state._value.text.length |
6285 | : atomicBoundary.getTrailingTextBoundaryAt(selection.baseOffset - 1) ?? 0, |
6286 | extentOffset: target, |
6287 | ); |
6288 | final ReplaceTextIntent replaceTextIntent = ReplaceTextIntent( |
6289 | state._value, |
6290 | '', |
6291 | rangeToDelete, |
6292 | SelectionChangedCause.keyboard, |
6293 | ); |
6294 | _hideToolbarIfTextChanged(replaceTextIntent); |
6295 | return Actions.invoke(context!, replaceTextIntent); |
6296 | } |
6297 | |
6298 | @override |
6299 | bool get isActionEnabled => !state.widget.readOnly && state._value.selection.isValid; |
6300 | } |
6301 | |
6302 | class _UpdateTextSelectionAction<T extends DirectionalCaretMovementIntent> |
6303 | extends ContextAction<T> { |
6304 | _UpdateTextSelectionAction( |
6305 | this.state, |
6306 | this.getTextBoundary, |
6307 | this.applyTextBoundary, { |
6308 | required this.ignoreNonCollapsedSelection, |
6309 | this.isExpand = false, |
6310 | this.extentAtIndex = false, |
6311 | }); |
6312 | |
6313 | final EditableTextState state; |
6314 | final bool ignoreNonCollapsedSelection; |
6315 | final bool isExpand; |
6316 | final bool extentAtIndex; |
6317 | final TextBoundary Function() getTextBoundary; |
6318 | final _ApplyTextBoundary applyTextBoundary; |
6319 | |
6320 | static const int NEWLINE_CODE_UNIT = 10; |
6321 | |
6322 | // Returns true iff the given position is at a wordwrap boundary in the |
6323 | // upstream position. |
6324 | bool _isAtWordwrapUpstream(TextPosition position) { |
6325 | final TextPosition end = TextPosition( |
6326 | offset: state.renderEditable.getLineAtOffset(position).end, |
6327 | affinity: TextAffinity.upstream, |
6328 | ); |
6329 | return end == position && |
6330 | end.offset != state.textEditingValue.text.length && |
6331 | state.textEditingValue.text.codeUnitAt(position.offset) != NEWLINE_CODE_UNIT; |
6332 | } |
6333 | |
6334 | // Returns true if the given position at a wordwrap boundary in the |
6335 | // downstream position. |
6336 | bool _isAtWordwrapDownstream(TextPosition position) { |
6337 | final TextPosition start = TextPosition( |
6338 | offset: state.renderEditable.getLineAtOffset(position).start, |
6339 | ); |
6340 | return start == position && |
6341 | start.offset != 0 && |
6342 | state.textEditingValue.text.codeUnitAt(position.offset - 1) != NEWLINE_CODE_UNIT; |
6343 | } |
6344 | |
6345 | @override |
6346 | Object? invoke(T intent, [BuildContext? context]) { |
6347 | final TextSelection selection = state._value.selection; |
6348 | assert(selection.isValid); |
6349 | |
6350 | final bool collapseSelection = intent.collapseSelection || !state.widget.selectionEnabled; |
6351 | if (!selection.isCollapsed && !ignoreNonCollapsedSelection && collapseSelection) { |
6352 | return Actions.invoke( |
6353 | context!, |
6354 | UpdateSelectionIntent( |
6355 | state._value, |
6356 | TextSelection.collapsed(offset: intent.forward ? selection.end : selection.start), |
6357 | SelectionChangedCause.keyboard, |
6358 | ), |
6359 | ); |
6360 | } |
6361 | |
6362 | TextPosition extent = selection.extent; |
6363 | // If continuesAtWrap is true extent and is at the relevant wordwrap, then |
6364 | // move it just to the other side of the wordwrap. |
6365 | if (intent.continuesAtWrap) { |
6366 | if (intent.forward && _isAtWordwrapUpstream(extent)) { |
6367 | extent = TextPosition(offset: extent.offset); |
6368 | } else if (!intent.forward && _isAtWordwrapDownstream(extent)) { |
6369 | extent = TextPosition(offset: extent.offset, affinity: TextAffinity.upstream); |
6370 | } |
6371 | } |
6372 | |
6373 | final bool shouldTargetBase = |
6374 | isExpand && |
6375 | (intent.forward |
6376 | ? selection.baseOffset > selection.extentOffset |
6377 | : selection.baseOffset < selection.extentOffset); |
6378 | final TextPosition newExtent = applyTextBoundary( |
6379 | shouldTargetBase ? selection.base : extent, |
6380 | intent.forward, |
6381 | getTextBoundary(), |
6382 | ); |
6383 | final TextSelection newSelection = |
6384 | collapseSelection || (!isExpand && newExtent.offset == selection.baseOffset) |
6385 | ? TextSelection.fromPosition(newExtent) |
6386 | : isExpand |
6387 | ? selection.expandTo(newExtent, extentAtIndex || selection.isCollapsed) |
6388 | : selection.extendTo(newExtent); |
6389 | |
6390 | final bool shouldCollapseToBase = |
6391 | intent.collapseAtReversal && |
6392 | (selection.baseOffset - selection.extentOffset) * |
6393 | (selection.baseOffset - newSelection.extentOffset) < |
6394 | 0; |
6395 | final TextSelection newRange = |
6396 | shouldCollapseToBase ? TextSelection.fromPosition(selection.base) : newSelection; |
6397 | return Actions.invoke( |
6398 | context!, |
6399 | UpdateSelectionIntent(state._value, newRange, SelectionChangedCause.keyboard), |
6400 | ); |
6401 | } |
6402 | |
6403 | @override |
6404 | bool get isActionEnabled => state._value.selection.isValid; |
6405 | } |
6406 | |
6407 | class _UpdateTextSelectionVerticallyAction<T extends DirectionalCaretMovementIntent> |
6408 | extends ContextAction<T> { |
6409 | _UpdateTextSelectionVerticallyAction(this.state); |
6410 | |
6411 | final EditableTextState state; |
6412 | |
6413 | VerticalCaretMovementRun? _verticalMovementRun; |
6414 | TextSelection? _runSelection; |
6415 | |
6416 | void stopCurrentVerticalRunIfSelectionChanges() { |
6417 | final TextSelection? runSelection = _runSelection; |
6418 | if (runSelection == null) { |
6419 | assert(_verticalMovementRun == null); |
6420 | return; |
6421 | } |
6422 | _runSelection = state._value.selection; |
6423 | final TextSelection currentSelection = state.widget.controller.selection; |
6424 | final bool continueCurrentRun = |
6425 | currentSelection.isValid && |
6426 | currentSelection.isCollapsed && |
6427 | currentSelection.baseOffset == runSelection.baseOffset && |
6428 | currentSelection.extentOffset == runSelection.extentOffset; |
6429 | if (!continueCurrentRun) { |
6430 | _verticalMovementRun = null; |
6431 | _runSelection = null; |
6432 | } |
6433 | } |
6434 | |
6435 | @override |
6436 | void invoke(T intent, [BuildContext? context]) { |
6437 | assert(state._value.selection.isValid); |
6438 | |
6439 | final bool collapseSelection = intent.collapseSelection || !state.widget.selectionEnabled; |
6440 | final TextEditingValue value = state._textEditingValueforTextLayoutMetrics; |
6441 | if (!value.selection.isValid) { |
6442 | return; |
6443 | } |
6444 | |
6445 | if (_verticalMovementRun?.isValid == false) { |
6446 | _verticalMovementRun = null; |
6447 | _runSelection = null; |
6448 | } |
6449 | |
6450 | final VerticalCaretMovementRun currentRun = |
6451 | _verticalMovementRun ?? |
6452 | state.renderEditable.startVerticalCaretMovement(state.renderEditable.selection!.extent); |
6453 | |
6454 | final bool shouldMove = |
6455 | intent is ExtendSelectionVerticallyToAdjacentPageIntent |
6456 | ? currentRun.moveByOffset( |
6457 | (intent.forward ? 1.0 : -1.0) * state.renderEditable.size.height, |
6458 | ) |
6459 | : intent.forward |
6460 | ? currentRun.moveNext() |
6461 | : currentRun.movePrevious(); |
6462 | final TextPosition newExtent = |
6463 | shouldMove |
6464 | ? currentRun.current |
6465 | : intent.forward |
6466 | ? TextPosition(offset: value.text.length) |
6467 | : const TextPosition(offset: 0); |
6468 | final TextSelection newSelection = |
6469 | collapseSelection |
6470 | ? TextSelection.fromPosition(newExtent) |
6471 | : value.selection.extendTo(newExtent); |
6472 | |
6473 | Actions.invoke( |
6474 | context!, |
6475 | UpdateSelectionIntent(value, newSelection, SelectionChangedCause.keyboard), |
6476 | ); |
6477 | if (state._value.selection == newSelection) { |
6478 | _verticalMovementRun = currentRun; |
6479 | _runSelection = newSelection; |
6480 | } |
6481 | } |
6482 | |
6483 | @override |
6484 | bool get isActionEnabled => state._value.selection.isValid; |
6485 | } |
6486 | |
6487 | class _SelectAllAction extends ContextAction<SelectAllTextIntent> { |
6488 | _SelectAllAction(this.state); |
6489 | |
6490 | final EditableTextState state; |
6491 | |
6492 | @override |
6493 | Object? invoke(SelectAllTextIntent intent, [BuildContext? context]) { |
6494 | return Actions.invoke( |
6495 | context!, |
6496 | UpdateSelectionIntent( |
6497 | state._value, |
6498 | TextSelection(baseOffset: 0, extentOffset: state._value.text.length), |
6499 | intent.cause, |
6500 | ), |
6501 | ); |
6502 | } |
6503 | |
6504 | @override |
6505 | bool get isActionEnabled => state.widget.selectionEnabled; |
6506 | } |
6507 | |
6508 | class _CopySelectionAction extends ContextAction<CopySelectionTextIntent> { |
6509 | _CopySelectionAction(this.state); |
6510 | |
6511 | final EditableTextState state; |
6512 | |
6513 | @override |
6514 | void invoke(CopySelectionTextIntent intent, [BuildContext? context]) { |
6515 | if (intent.collapseSelection) { |
6516 | state.cutSelection(intent.cause); |
6517 | } else { |
6518 | state.copySelection(intent.cause); |
6519 | } |
6520 | } |
6521 | |
6522 | @override |
6523 | bool get isActionEnabled => state._value.selection.isValid && !state._value.selection.isCollapsed; |
6524 | } |
6525 | |
6526 | /// A [ClipboardStatusNotifier] whose [value] is hardcoded to |
6527 | /// [ClipboardStatus.pasteable]. |
6528 | /// |
6529 | /// Useful to avoid showing a permission dialog on web, which happens when |
6530 | /// [Clipboard.hasStrings] is called. |
6531 | class _WebClipboardStatusNotifier extends ClipboardStatusNotifier { |
6532 | @override |
6533 | ClipboardStatus value = ClipboardStatus.pasteable; |
6534 | |
6535 | @override |
6536 | Future<void> update() { |
6537 | return Future<void>.value(); |
6538 | } |
6539 | } |
6540 | |
6541 | class _EditableTextTapOutsideAction extends ContextAction<EditableTextTapOutsideIntent> { |
6542 | _EditableTextTapOutsideAction(); |
6543 | |
6544 | @override |
6545 | void invoke(EditableTextTapOutsideIntent intent, [BuildContext? context]) { |
6546 | // The focus dropping behavior is only present on desktop platforms. |
6547 | switch (defaultTargetPlatform) { |
6548 | case TargetPlatform.android: |
6549 | case TargetPlatform.iOS: |
6550 | case TargetPlatform.fuchsia: |
6551 | // On mobile platforms, we don't unfocus on touch events unless they're |
6552 | // in the web browser, but we do unfocus for all other kinds of events. |
6553 | switch (intent.pointerDownEvent.kind) { |
6554 | case ui.PointerDeviceKind.touch: |
6555 | if (kIsWeb) { |
6556 | intent.focusNode.unfocus(); |
6557 | } |
6558 | case ui.PointerDeviceKind.mouse: |
6559 | case ui.PointerDeviceKind.stylus: |
6560 | case ui.PointerDeviceKind.invertedStylus: |
6561 | case ui.PointerDeviceKind.unknown: |
6562 | intent.focusNode.unfocus(); |
6563 | case ui.PointerDeviceKind.trackpad: |
6564 | throw UnimplementedError('Unexpected pointer down event for trackpad'); |
6565 | } |
6566 | case TargetPlatform.linux: |
6567 | case TargetPlatform.macOS: |
6568 | case TargetPlatform.windows: |
6569 | intent.focusNode.unfocus(); |
6570 | } |
6571 | } |
6572 | } |
6573 | |
6574 | class _EditableTextTapUpOutsideAction extends ContextAction<EditableTextTapUpOutsideIntent> { |
6575 | _EditableTextTapUpOutsideAction(); |
6576 | |
6577 | @override |
6578 | void invoke(EditableTextTapUpOutsideIntent intent, [BuildContext? context]) { |
6579 | // The default action is a no-op. |
6580 | } |
6581 | } |
6582 |
Definitions
- _kCursorBlinkHalfPeriod
- _kObscureShowLatestCharCursorTicks
- kDefaultContentInsertionMimeTypes
- _CompositionCallback
- _CompositionCallback
- createRenderObject
- updateRenderObject
- _RenderCompositionCallback
- _RenderCompositionCallback
- enabled
- enabled
- paint
- TextEditingController
- TextEditingController
- fromValue
- text
- text
- value
- buildTextSpan
- selection
- selection
- clear
- clearComposing
- _isSelectionWithinComposingRange
- ToolbarOptions
- ToolbarOptions
- ContentInsertionConfiguration
- ContentInsertionConfiguration
- _KeyFrame
- _KeyFrame
- _DiscreteKeyFrameSimulation
- iOSBlinkingCaret
- _
- dx
- isDone
Learn more about Flutter for embedded and desktop on industrialflutter.com