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 'color_scheme.dart'; |
6 | /// @docImport 'scaffold.dart'; |
7 | /// @docImport 'selection_area.dart'; |
8 | /// @docImport 'text_field.dart'; |
9 | library; |
10 | |
11 | import 'dart:ui' as ui show BoxHeightStyle, BoxWidthStyle; |
12 | |
13 | import 'package:flutter/cupertino.dart'; |
14 | import 'package:flutter/foundation.dart'; |
15 | import 'package:flutter/gestures.dart'; |
16 | import 'package:flutter/rendering.dart'; |
17 | import 'package:flutter/scheduler.dart'; |
18 | |
19 | import 'adaptive_text_selection_toolbar.dart'; |
20 | import 'desktop_text_selection.dart'; |
21 | import 'magnifier.dart'; |
22 | import 'text_selection.dart'; |
23 | import 'theme.dart'; |
24 | |
25 | // Examples can assume: |
26 | // late BuildContext context; |
27 | // late FocusNode myFocusNode; |
28 | |
29 | /// An eyeballed value that moves the cursor slightly left of where it is |
30 | /// rendered for text on Android so its positioning more accurately matches the |
31 | /// native iOS text cursor positioning. |
32 | /// |
33 | /// This value is in device pixels, not logical pixels as is typically used |
34 | /// throughout the codebase. |
35 | const int iOSHorizontalOffset = -2; |
36 | |
37 | class _TextSpanEditingController extends TextEditingController { |
38 | _TextSpanEditingController({required TextSpan textSpan}) |
39 | : _textSpan = textSpan, |
40 | super(text: textSpan.toPlainText(includeSemanticsLabels: false)); |
41 | |
42 | final TextSpan _textSpan; |
43 | |
44 | @override |
45 | TextSpan buildTextSpan({ |
46 | required BuildContext context, |
47 | TextStyle? style, |
48 | required bool withComposing, |
49 | }) { |
50 | // This does not care about composing. |
51 | return TextSpan(style: style, children: <TextSpan>[_textSpan]); |
52 | } |
53 | |
54 | @override |
55 | set text(String? newText) { |
56 | // This should never be reached. |
57 | throw UnimplementedError(); |
58 | } |
59 | } |
60 | |
61 | class _SelectableTextSelectionGestureDetectorBuilder extends TextSelectionGestureDetectorBuilder { |
62 | _SelectableTextSelectionGestureDetectorBuilder({required _SelectableTextState state}) |
63 | : _state = state, |
64 | super(delegate: state); |
65 | |
66 | final _SelectableTextState _state; |
67 | |
68 | @override |
69 | void onSingleTapUp(TapDragUpDetails details) { |
70 | if (!delegate.selectionEnabled) { |
71 | return; |
72 | } |
73 | super.onSingleTapUp(details); |
74 | _state.widget.onTap?.call(); |
75 | } |
76 | } |
77 | |
78 | /// A run of selectable text with a single style. |
79 | /// |
80 | /// Consider using [SelectionArea] or [SelectableRegion] instead, which enable |
81 | /// selection on a widget subtree, including but not limited to [Text] widgets. |
82 | /// |
83 | /// The [SelectableText] widget displays a string of text with a single style. |
84 | /// The string might break across multiple lines or might all be displayed on |
85 | /// the same line depending on the layout constraints. |
86 | /// |
87 | /// {@youtube 560 315 https://www.youtube.com/watch?v=ZSU3ZXOs6hc} |
88 | /// |
89 | /// The [style] argument is optional. When omitted, the text will use the style |
90 | /// from the closest enclosing [DefaultTextStyle]. If the given style's |
91 | /// [TextStyle.inherit] property is true (the default), the given style will |
92 | /// be merged with the closest enclosing [DefaultTextStyle]. This merging |
93 | /// behavior is useful, for example, to make the text bold while using the |
94 | /// default font family and size. |
95 | /// |
96 | /// {@macro flutter.material.textfield.wantKeepAlive} |
97 | /// |
98 | /// {@tool snippet} |
99 | /// |
100 | /// ```dart |
101 | /// const SelectableText( |
102 | /// 'Hello! How are you?', |
103 | /// textAlign: TextAlign.center, |
104 | /// style: TextStyle(fontWeight: FontWeight.bold), |
105 | /// ) |
106 | /// ``` |
107 | /// {@end-tool} |
108 | /// |
109 | /// Using the [SelectableText.rich] constructor, the [SelectableText] widget can |
110 | /// display a paragraph with differently styled [TextSpan]s. The sample |
111 | /// that follows displays "Hello beautiful world" with different styles |
112 | /// for each word. |
113 | /// |
114 | /// {@tool snippet} |
115 | /// |
116 | /// ```dart |
117 | /// const SelectableText.rich( |
118 | /// TextSpan( |
119 | /// text: 'Hello', // default text style |
120 | /// children: <TextSpan>[ |
121 | /// TextSpan(text: ' beautiful ', style: TextStyle(fontStyle: FontStyle.italic)), |
122 | /// TextSpan(text: 'world', style: TextStyle(fontWeight: FontWeight.bold)), |
123 | /// ], |
124 | /// ), |
125 | /// ) |
126 | /// ``` |
127 | /// {@end-tool} |
128 | /// |
129 | /// ## Interactivity |
130 | /// |
131 | /// To make [SelectableText] react to touch events, use callback [onTap] to achieve |
132 | /// the desired behavior. |
133 | /// |
134 | /// ## Scrolling Considerations |
135 | /// |
136 | /// If this [SelectableText] is not a descendant of [Scaffold] and is being used |
137 | /// within a [Scrollable] or nested [Scrollable]s, consider placing a |
138 | /// [ScrollNotificationObserver] above the root [Scrollable] that contains this |
139 | /// [SelectableText] to ensure proper scroll coordination for [SelectableText] |
140 | /// and its components like [TextSelectionOverlay]. |
141 | /// |
142 | /// See also: |
143 | /// |
144 | /// * [Text], which is the non selectable version of this widget. |
145 | /// * [TextField], which is the editable version of this widget. |
146 | /// * [SelectionArea], which enables the selection of multiple [Text] widgets |
147 | /// and of other widgets. |
148 | class SelectableText extends StatefulWidget { |
149 | /// Creates a selectable text widget. |
150 | /// |
151 | /// If the [style] argument is null, the text will use the style from the |
152 | /// closest enclosing [DefaultTextStyle]. |
153 | /// |
154 | |
155 | /// If the [showCursor], [autofocus], [dragStartBehavior], |
156 | /// [selectionHeightStyle], [selectionWidthStyle] and [data] arguments are |
157 | /// specified, the [maxLines] argument must be greater than zero. |
158 | const SelectableText( |
159 | String this.data, { |
160 | super.key, |
161 | this.focusNode, |
162 | this.style, |
163 | this.strutStyle, |
164 | this.textAlign, |
165 | this.textDirection, |
166 | @Deprecated( |
167 | 'Use textScaler instead. ' |
168 | 'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. ' |
169 | 'This feature was deprecated after v3.12.0-2.0.pre.', |
170 | ) |
171 | this.textScaleFactor, |
172 | this.textScaler, |
173 | this.showCursor = false, |
174 | this.autofocus = false, |
175 | @Deprecated( |
176 | 'Use `contextMenuBuilder` instead. ' |
177 | 'This feature was deprecated after v3.3.0-0.5.pre.', |
178 | ) |
179 | this.toolbarOptions, |
180 | this.minLines, |
181 | this.maxLines, |
182 | this.cursorWidth = 2.0, |
183 | this.cursorHeight, |
184 | this.cursorRadius, |
185 | this.cursorColor, |
186 | this.selectionColor, |
187 | this.selectionHeightStyle = ui.BoxHeightStyle.tight, |
188 | this.selectionWidthStyle = ui.BoxWidthStyle.tight, |
189 | this.dragStartBehavior = DragStartBehavior.start, |
190 | this.enableInteractiveSelection = true, |
191 | this.selectionControls, |
192 | this.onTap, |
193 | this.scrollPhysics, |
194 | this.scrollBehavior, |
195 | this.semanticsLabel, |
196 | this.textHeightBehavior, |
197 | this.textWidthBasis, |
198 | this.onSelectionChanged, |
199 | this.contextMenuBuilder = _defaultContextMenuBuilder, |
200 | this.magnifierConfiguration, |
201 | }) : assert(maxLines == null || maxLines > 0), |
202 | assert(minLines == null || minLines > 0), |
203 | assert( |
204 | (maxLines == null) || (minLines == null) || (maxLines >= minLines), |
205 | "minLines can't be greater than maxLines", |
206 | ), |
207 | assert( |
208 | textScaler == null || textScaleFactor == null, |
209 | 'textScaleFactor is deprecated and cannot be specified when textScaler is specified.', |
210 | ), |
211 | textSpan = null; |
212 | |
213 | /// Creates a selectable text widget with a [TextSpan]. |
214 | /// |
215 | /// The [TextSpan.children] attribute of the [textSpan] parameter must only |
216 | /// contain [TextSpan]s. Other types of [InlineSpan] are not allowed. |
217 | const SelectableText.rich( |
218 | TextSpan this.textSpan, { |
219 | super.key, |
220 | this.focusNode, |
221 | this.style, |
222 | this.strutStyle, |
223 | this.textAlign, |
224 | this.textDirection, |
225 | @Deprecated( |
226 | 'Use textScaler instead. ' |
227 | 'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. ' |
228 | 'This feature was deprecated after v3.12.0-2.0.pre.', |
229 | ) |
230 | this.textScaleFactor, |
231 | this.textScaler, |
232 | this.showCursor = false, |
233 | this.autofocus = false, |
234 | @Deprecated( |
235 | 'Use `contextMenuBuilder` instead. ' |
236 | 'This feature was deprecated after v3.3.0-0.5.pre.', |
237 | ) |
238 | this.toolbarOptions, |
239 | this.minLines, |
240 | this.maxLines, |
241 | this.cursorWidth = 2.0, |
242 | this.cursorHeight, |
243 | this.cursorRadius, |
244 | this.cursorColor, |
245 | this.selectionColor, |
246 | this.selectionHeightStyle = ui.BoxHeightStyle.tight, |
247 | this.selectionWidthStyle = ui.BoxWidthStyle.tight, |
248 | this.dragStartBehavior = DragStartBehavior.start, |
249 | this.enableInteractiveSelection = true, |
250 | this.selectionControls, |
251 | this.onTap, |
252 | this.scrollPhysics, |
253 | this.scrollBehavior, |
254 | this.semanticsLabel, |
255 | this.textHeightBehavior, |
256 | this.textWidthBasis, |
257 | this.onSelectionChanged, |
258 | this.contextMenuBuilder = _defaultContextMenuBuilder, |
259 | this.magnifierConfiguration, |
260 | }) : assert(maxLines == null || maxLines > 0), |
261 | assert(minLines == null || minLines > 0), |
262 | assert( |
263 | (maxLines == null) || (minLines == null) || (maxLines >= minLines), |
264 | "minLines can't be greater than maxLines", |
265 | ), |
266 | assert( |
267 | textScaler == null || textScaleFactor == null, |
268 | 'textScaleFactor is deprecated and cannot be specified when textScaler is specified.', |
269 | ), |
270 | data = null; |
271 | |
272 | /// The text to display. |
273 | /// |
274 | /// This will be null if a [textSpan] is provided instead. |
275 | final String? data; |
276 | |
277 | /// The text to display as a [TextSpan]. |
278 | /// |
279 | /// This will be null if [data] is provided instead. |
280 | final TextSpan? textSpan; |
281 | |
282 | /// Defines the focus for this widget. |
283 | /// |
284 | /// Text is only selectable when widget is focused. |
285 | /// |
286 | /// The [focusNode] is a long-lived object that's typically managed by a |
287 | /// [StatefulWidget] parent. See [FocusNode] for more information. |
288 | /// |
289 | /// To give the focus to this widget, provide a [focusNode] and then |
290 | /// use the current [FocusScope] to request the focus: |
291 | /// |
292 | /// ```dart |
293 | /// FocusScope.of(context).requestFocus(myFocusNode); |
294 | /// ``` |
295 | /// |
296 | /// This happens automatically when the widget is tapped. |
297 | /// |
298 | /// To be notified when the widget gains or loses the focus, add a listener |
299 | /// to the [focusNode]: |
300 | /// |
301 | /// ```dart |
302 | /// myFocusNode.addListener(() { print(myFocusNode.hasFocus); }); |
303 | /// ``` |
304 | /// |
305 | /// If null, this widget will create its own [FocusNode] with |
306 | /// [FocusNode.skipTraversal] parameter set to `true`, which causes the widget |
307 | /// to be skipped over during focus traversal. |
308 | final FocusNode? focusNode; |
309 | |
310 | /// The style to use for the text. |
311 | /// |
312 | /// If null, defaults [DefaultTextStyle] of context. |
313 | final TextStyle? style; |
314 | |
315 | /// {@macro flutter.widgets.editableText.strutStyle} |
316 | final StrutStyle? strutStyle; |
317 | |
318 | /// {@macro flutter.widgets.editableText.textAlign} |
319 | final TextAlign? textAlign; |
320 | |
321 | /// {@macro flutter.widgets.editableText.textDirection} |
322 | final TextDirection? textDirection; |
323 | |
324 | /// {@macro flutter.widgets.editableText.textScaleFactor} |
325 | @Deprecated( |
326 | 'Use textScaler instead. ' |
327 | 'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. ' |
328 | 'This feature was deprecated after v3.12.0-2.0.pre.', |
329 | ) |
330 | final double? textScaleFactor; |
331 | |
332 | /// {@macro flutter.painting.textPainter.textScaler} |
333 | final TextScaler? textScaler; |
334 | |
335 | /// {@macro flutter.widgets.editableText.autofocus} |
336 | final bool autofocus; |
337 | |
338 | /// {@macro flutter.widgets.editableText.minLines} |
339 | final int? minLines; |
340 | |
341 | /// {@macro flutter.widgets.editableText.maxLines} |
342 | final int? maxLines; |
343 | |
344 | /// {@macro flutter.widgets.editableText.showCursor} |
345 | final bool showCursor; |
346 | |
347 | /// {@macro flutter.widgets.editableText.cursorWidth} |
348 | final double cursorWidth; |
349 | |
350 | /// {@macro flutter.widgets.editableText.cursorHeight} |
351 | final double? cursorHeight; |
352 | |
353 | /// {@macro flutter.widgets.editableText.cursorRadius} |
354 | final Radius? cursorRadius; |
355 | |
356 | /// The color of the cursor. |
357 | /// |
358 | /// The cursor indicates the current text insertion point. |
359 | /// |
360 | /// If null then [DefaultSelectionStyle.cursorColor] is used. If that is also |
361 | /// null and [ThemeData.platform] is [TargetPlatform.iOS] or |
362 | /// [TargetPlatform.macOS], then [CupertinoThemeData.primaryColor] is used. |
363 | /// Otherwise [ColorScheme.primary] of [ThemeData.colorScheme] is used. |
364 | final Color? cursorColor; |
365 | |
366 | /// The color to use when painting the selection. |
367 | /// |
368 | /// If this property is null, this widget gets the selection color from the |
369 | /// inherited [DefaultSelectionStyle] (if any); if none, the selection |
370 | /// color is derived from the [CupertinoThemeData.primaryColor] on |
371 | /// Apple platforms and [ColorScheme.primary] of [ThemeData.colorScheme] on |
372 | /// other platforms. |
373 | final Color? selectionColor; |
374 | |
375 | /// Controls how tall the selection highlight boxes are computed to be. |
376 | /// |
377 | /// See [ui.BoxHeightStyle] for details on available styles. |
378 | final ui.BoxHeightStyle selectionHeightStyle; |
379 | |
380 | /// Controls how wide the selection highlight boxes are computed to be. |
381 | /// |
382 | /// See [ui.BoxWidthStyle] for details on available styles. |
383 | final ui.BoxWidthStyle selectionWidthStyle; |
384 | |
385 | /// {@macro flutter.widgets.editableText.enableInteractiveSelection} |
386 | final bool enableInteractiveSelection; |
387 | |
388 | /// {@macro flutter.widgets.editableText.selectionControls} |
389 | final TextSelectionControls? selectionControls; |
390 | |
391 | /// {@macro flutter.widgets.scrollable.dragStartBehavior} |
392 | final DragStartBehavior dragStartBehavior; |
393 | |
394 | /// Configuration of toolbar options. |
395 | /// |
396 | /// Paste and cut will be disabled regardless. |
397 | /// |
398 | /// If not set, select all and copy will be enabled by default. |
399 | @Deprecated( |
400 | 'Use `contextMenuBuilder` instead. ' |
401 | 'This feature was deprecated after v3.3.0-0.5.pre.', |
402 | ) |
403 | final ToolbarOptions? toolbarOptions; |
404 | |
405 | /// {@macro flutter.widgets.editableText.selectionEnabled} |
406 | bool get selectionEnabled => enableInteractiveSelection; |
407 | |
408 | /// Called when the user taps on this selectable text. |
409 | /// |
410 | /// The selectable text builds a [GestureDetector] to handle input events like tap, |
411 | /// to trigger focus requests, to move the caret, adjust the selection, etc. |
412 | /// Handling some of those events by wrapping the selectable text with a competing |
413 | /// GestureDetector is problematic. |
414 | /// |
415 | /// To unconditionally handle taps, without interfering with the selectable text's |
416 | /// internal gesture detector, provide this callback. |
417 | /// |
418 | /// To be notified when the text field gains or loses the focus, provide a |
419 | /// [focusNode] and add a listener to that. |
420 | /// |
421 | /// To listen to arbitrary pointer events without competing with the |
422 | /// selectable text's internal gesture detector, use a [Listener]. |
423 | final GestureTapCallback? onTap; |
424 | |
425 | /// {@macro flutter.widgets.editableText.scrollPhysics} |
426 | final ScrollPhysics? scrollPhysics; |
427 | |
428 | /// {@macro flutter.widgets.editableText.scrollBehavior} |
429 | final ScrollBehavior? scrollBehavior; |
430 | |
431 | /// {@macro flutter.widgets.Text.semanticsLabel} |
432 | final String? semanticsLabel; |
433 | |
434 | /// {@macro dart.ui.textHeightBehavior} |
435 | final TextHeightBehavior? textHeightBehavior; |
436 | |
437 | /// {@macro flutter.painting.textPainter.textWidthBasis} |
438 | final TextWidthBasis? textWidthBasis; |
439 | |
440 | /// {@macro flutter.widgets.editableText.onSelectionChanged} |
441 | final SelectionChangedCallback? onSelectionChanged; |
442 | |
443 | /// {@macro flutter.widgets.EditableText.contextMenuBuilder} |
444 | final EditableTextContextMenuBuilder? contextMenuBuilder; |
445 | |
446 | static Widget _defaultContextMenuBuilder( |
447 | BuildContext context, |
448 | EditableTextState editableTextState, |
449 | ) { |
450 | if (defaultTargetPlatform == TargetPlatform.iOS && SystemContextMenu.isSupported(context)) { |
451 | return SystemContextMenu.editableText(editableTextState: editableTextState); |
452 | } |
453 | return AdaptiveTextSelectionToolbar.editableText(editableTextState: editableTextState); |
454 | } |
455 | |
456 | /// The configuration for the magnifier used when the text is selected. |
457 | /// |
458 | /// By default, builds a [CupertinoTextMagnifier] on iOS and [TextMagnifier] |
459 | /// on Android, and builds nothing on all other platforms. To suppress the |
460 | /// magnifier, consider passing [TextMagnifierConfiguration.disabled]. |
461 | /// |
462 | /// {@macro flutter.widgets.magnifier.intro} |
463 | final TextMagnifierConfiguration? magnifierConfiguration; |
464 | |
465 | @override |
466 | State<SelectableText> createState() => _SelectableTextState(); |
467 | |
468 | @override |
469 | void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
470 | super.debugFillProperties(properties); |
471 | properties.add(DiagnosticsProperty<String>('data', data, defaultValue: null)); |
472 | properties.add( |
473 | DiagnosticsProperty<String>('semanticsLabel', semanticsLabel, defaultValue: null), |
474 | ); |
475 | properties.add(DiagnosticsProperty<FocusNode>('focusNode', focusNode, defaultValue: null)); |
476 | properties.add(DiagnosticsProperty<TextStyle>('style', style, defaultValue: null)); |
477 | properties.add(DiagnosticsProperty<bool>('autofocus', autofocus, defaultValue: false)); |
478 | properties.add(DiagnosticsProperty<bool>('showCursor', showCursor, defaultValue: false)); |
479 | properties.add(IntProperty('minLines', minLines, defaultValue: null)); |
480 | properties.add(IntProperty('maxLines', maxLines, defaultValue: null)); |
481 | properties.add(EnumProperty<TextAlign>('textAlign', textAlign, defaultValue: null)); |
482 | properties.add(EnumProperty<TextDirection>('textDirection', textDirection, defaultValue: null)); |
483 | properties.add(DoubleProperty('textScaleFactor', textScaleFactor, defaultValue: null)); |
484 | properties.add(DiagnosticsProperty<TextScaler>('textScaler', textScaler, defaultValue: null)); |
485 | properties.add(DoubleProperty('cursorWidth', cursorWidth, defaultValue: 2.0)); |
486 | properties.add(DoubleProperty('cursorHeight', cursorHeight, defaultValue: null)); |
487 | properties.add(DiagnosticsProperty<Radius>('cursorRadius', cursorRadius, defaultValue: null)); |
488 | properties.add(DiagnosticsProperty<Color>('cursorColor', cursorColor, defaultValue: null)); |
489 | properties.add( |
490 | DiagnosticsProperty<Color>('selectionColor', selectionColor, defaultValue: null), |
491 | ); |
492 | properties.add( |
493 | FlagProperty( |
494 | 'selectionEnabled', |
495 | value: selectionEnabled, |
496 | defaultValue: true, |
497 | ifFalse: 'selection disabled', |
498 | ), |
499 | ); |
500 | properties.add( |
501 | DiagnosticsProperty<TextSelectionControls>( |
502 | 'selectionControls', |
503 | selectionControls, |
504 | defaultValue: null, |
505 | ), |
506 | ); |
507 | properties.add( |
508 | DiagnosticsProperty<ScrollPhysics>('scrollPhysics', scrollPhysics, defaultValue: null), |
509 | ); |
510 | properties.add( |
511 | DiagnosticsProperty<ScrollBehavior>('scrollBehavior', scrollBehavior, defaultValue: null), |
512 | ); |
513 | properties.add( |
514 | DiagnosticsProperty<TextHeightBehavior>( |
515 | 'textHeightBehavior', |
516 | textHeightBehavior, |
517 | defaultValue: null, |
518 | ), |
519 | ); |
520 | } |
521 | } |
522 | |
523 | class _SelectableTextState extends State<SelectableText> |
524 | implements TextSelectionGestureDetectorBuilderDelegate { |
525 | EditableTextState? get _editableText => editableTextKey.currentState; |
526 | |
527 | late _TextSpanEditingController _controller; |
528 | |
529 | FocusNode? _focusNode; |
530 | FocusNode get _effectiveFocusNode => |
531 | widget.focusNode ?? (_focusNode ??= FocusNode(skipTraversal: true)); |
532 | |
533 | bool _showSelectionHandles = false; |
534 | |
535 | late _SelectableTextSelectionGestureDetectorBuilder _selectionGestureDetectorBuilder; |
536 | |
537 | // API for TextSelectionGestureDetectorBuilderDelegate. |
538 | @override |
539 | late bool forcePressEnabled; |
540 | |
541 | @override |
542 | final GlobalKey<EditableTextState> editableTextKey = GlobalKey<EditableTextState>(); |
543 | |
544 | @override |
545 | bool get selectionEnabled => widget.selectionEnabled; |
546 | // End of API for TextSelectionGestureDetectorBuilderDelegate. |
547 | |
548 | @override |
549 | void initState() { |
550 | super.initState(); |
551 | _selectionGestureDetectorBuilder = _SelectableTextSelectionGestureDetectorBuilder(state: this); |
552 | _controller = _TextSpanEditingController( |
553 | textSpan: widget.textSpan ?? TextSpan(text: widget.data), |
554 | ); |
555 | _controller.addListener(_onControllerChanged); |
556 | _effectiveFocusNode.addListener(_handleFocusChanged); |
557 | } |
558 | |
559 | @override |
560 | void didUpdateWidget(SelectableText oldWidget) { |
561 | super.didUpdateWidget(oldWidget); |
562 | if (widget.data != oldWidget.data || widget.textSpan != oldWidget.textSpan) { |
563 | _controller.removeListener(_onControllerChanged); |
564 | _controller.dispose(); |
565 | _controller = _TextSpanEditingController( |
566 | textSpan: widget.textSpan ?? TextSpan(text: widget.data), |
567 | ); |
568 | _controller.addListener(_onControllerChanged); |
569 | } |
570 | if (widget.focusNode != oldWidget.focusNode) { |
571 | (oldWidget.focusNode ?? _focusNode)?.removeListener(_handleFocusChanged); |
572 | (widget.focusNode ?? _focusNode)?.addListener(_handleFocusChanged); |
573 | } |
574 | if (_effectiveFocusNode.hasFocus && _controller.selection.isCollapsed) { |
575 | _showSelectionHandles = false; |
576 | } else { |
577 | _showSelectionHandles = true; |
578 | } |
579 | } |
580 | |
581 | @override |
582 | void dispose() { |
583 | _effectiveFocusNode.removeListener(_handleFocusChanged); |
584 | _focusNode?.dispose(); |
585 | _controller.dispose(); |
586 | super.dispose(); |
587 | } |
588 | |
589 | void _onControllerChanged() { |
590 | final bool showSelectionHandles = |
591 | !_effectiveFocusNode.hasFocus || !_controller.selection.isCollapsed; |
592 | if (showSelectionHandles == _showSelectionHandles) { |
593 | return; |
594 | } |
595 | setState(() { |
596 | _showSelectionHandles = showSelectionHandles; |
597 | }); |
598 | } |
599 | |
600 | void _handleFocusChanged() { |
601 | if (!_effectiveFocusNode.hasFocus && |
602 | SchedulerBinding.instance.lifecycleState == AppLifecycleState.resumed) { |
603 | // We should only clear the selection when this SelectableText loses |
604 | // focus while the application is currently running. It is possible |
605 | // that the application is not currently running, for example on desktop |
606 | // platforms, clicking on a different window switches the focus to |
607 | // the new window causing the Flutter application to go inactive. In this |
608 | // case we want to retain the selection so it remains when we return to |
609 | // the Flutter application. |
610 | _controller.value = TextEditingValue(text: _controller.value.text); |
611 | } |
612 | } |
613 | |
614 | void _handleSelectionChanged(TextSelection selection, SelectionChangedCause? cause) { |
615 | final bool willShowSelectionHandles = _shouldShowSelectionHandles(cause); |
616 | if (willShowSelectionHandles != _showSelectionHandles) { |
617 | setState(() { |
618 | _showSelectionHandles = willShowSelectionHandles; |
619 | }); |
620 | } |
621 | |
622 | widget.onSelectionChanged?.call(selection, cause); |
623 | |
624 | switch (Theme.of(context).platform) { |
625 | case TargetPlatform.iOS: |
626 | case TargetPlatform.macOS: |
627 | if (cause == SelectionChangedCause.longPress) { |
628 | _editableText?.bringIntoView(selection.base); |
629 | } |
630 | return; |
631 | case TargetPlatform.android: |
632 | case TargetPlatform.fuchsia: |
633 | case TargetPlatform.linux: |
634 | case TargetPlatform.windows: |
635 | // Do nothing. |
636 | } |
637 | } |
638 | |
639 | /// Toggle the toolbar when a selection handle is tapped. |
640 | void _handleSelectionHandleTapped() { |
641 | if (_controller.selection.isCollapsed) { |
642 | _editableText!.toggleToolbar(); |
643 | } |
644 | } |
645 | |
646 | bool _shouldShowSelectionHandles(SelectionChangedCause? cause) { |
647 | // When the text field is activated by something that doesn't trigger the |
648 | // selection overlay, we shouldn't show the handles either. |
649 | if (!_selectionGestureDetectorBuilder.shouldShowSelectionToolbar) { |
650 | return false; |
651 | } |
652 | |
653 | if (_controller.selection.isCollapsed) { |
654 | return false; |
655 | } |
656 | |
657 | if (cause == SelectionChangedCause.keyboard) { |
658 | return false; |
659 | } |
660 | |
661 | if (cause == SelectionChangedCause.longPress) { |
662 | return true; |
663 | } |
664 | |
665 | if (_controller.text.isNotEmpty) { |
666 | return true; |
667 | } |
668 | |
669 | return false; |
670 | } |
671 | |
672 | @override |
673 | Widget build(BuildContext context) { |
674 | // TODO(garyq): Assert to block WidgetSpans from being used here are removed, |
675 | // but we still do not yet have nice handling of things like carets, clipboard, |
676 | // and other features. We should add proper support. Currently, caret handling |
677 | // is blocked on SkParagraph switch and https://github.com/flutter/engine/pull/27010 |
678 | // should be landed in SkParagraph after the switch is complete. |
679 | assert(debugCheckHasMediaQuery(context)); |
680 | assert(debugCheckHasDirectionality(context)); |
681 | assert( |
682 | !(widget.style != null && |
683 | !widget.style!.inherit && |
684 | (widget.style!.fontSize == null || widget.style!.textBaseline == null)), |
685 | 'inherit false style must supply fontSize and textBaseline', |
686 | ); |
687 | |
688 | final ThemeData theme = Theme.of(context); |
689 | final DefaultSelectionStyle selectionStyle = DefaultSelectionStyle.of(context); |
690 | final FocusNode focusNode = _effectiveFocusNode; |
691 | |
692 | TextSelectionControls? textSelectionControls = widget.selectionControls; |
693 | final bool paintCursorAboveText; |
694 | final bool cursorOpacityAnimates; |
695 | Offset? cursorOffset; |
696 | final Color cursorColor; |
697 | final Color selectionColor; |
698 | Radius? cursorRadius = widget.cursorRadius; |
699 | |
700 | switch (theme.platform) { |
701 | case TargetPlatform.iOS: |
702 | final CupertinoThemeData cupertinoTheme = CupertinoTheme.of(context); |
703 | forcePressEnabled = true; |
704 | textSelectionControls ??= cupertinoTextSelectionHandleControls; |
705 | paintCursorAboveText = true; |
706 | cursorOpacityAnimates = true; |
707 | cursorColor = |
708 | widget.cursorColor ?? selectionStyle.cursorColor ?? cupertinoTheme.primaryColor; |
709 | selectionColor = |
710 | selectionStyle.selectionColor ?? cupertinoTheme.primaryColor.withOpacity(0.40); |
711 | cursorRadius ??= const Radius.circular(2.0); |
712 | cursorOffset = Offset(iOSHorizontalOffset / MediaQuery.devicePixelRatioOf(context), 0); |
713 | |
714 | case TargetPlatform.macOS: |
715 | final CupertinoThemeData cupertinoTheme = CupertinoTheme.of(context); |
716 | forcePressEnabled = false; |
717 | textSelectionControls ??= cupertinoDesktopTextSelectionHandleControls; |
718 | paintCursorAboveText = true; |
719 | cursorOpacityAnimates = true; |
720 | cursorColor = |
721 | widget.cursorColor ?? selectionStyle.cursorColor ?? cupertinoTheme.primaryColor; |
722 | selectionColor = |
723 | selectionStyle.selectionColor ?? cupertinoTheme.primaryColor.withOpacity(0.40); |
724 | cursorRadius ??= const Radius.circular(2.0); |
725 | cursorOffset = Offset(iOSHorizontalOffset / MediaQuery.devicePixelRatioOf(context), 0); |
726 | |
727 | case TargetPlatform.android: |
728 | case TargetPlatform.fuchsia: |
729 | forcePressEnabled = false; |
730 | textSelectionControls ??= materialTextSelectionHandleControls; |
731 | paintCursorAboveText = false; |
732 | cursorOpacityAnimates = false; |
733 | cursorColor = widget.cursorColor ?? selectionStyle.cursorColor ?? theme.colorScheme.primary; |
734 | selectionColor = |
735 | selectionStyle.selectionColor ?? theme.colorScheme.primary.withOpacity(0.40); |
736 | |
737 | case TargetPlatform.linux: |
738 | case TargetPlatform.windows: |
739 | forcePressEnabled = false; |
740 | textSelectionControls ??= desktopTextSelectionHandleControls; |
741 | paintCursorAboveText = false; |
742 | cursorOpacityAnimates = false; |
743 | cursorColor = widget.cursorColor ?? selectionStyle.cursorColor ?? theme.colorScheme.primary; |
744 | selectionColor = |
745 | selectionStyle.selectionColor ?? theme.colorScheme.primary.withOpacity(0.40); |
746 | } |
747 | |
748 | final DefaultTextStyle defaultTextStyle = DefaultTextStyle.of(context); |
749 | TextStyle? effectiveTextStyle = widget.style; |
750 | if (effectiveTextStyle == null || effectiveTextStyle.inherit) { |
751 | effectiveTextStyle = defaultTextStyle.style.merge( |
752 | widget.style ?? _controller._textSpan.style, |
753 | ); |
754 | } |
755 | final TextScaler? effectiveScaler = |
756 | widget.textScaler ?? |
757 | switch (widget.textScaleFactor) { |
758 | null => null, |
759 | final double textScaleFactor => TextScaler.linear(textScaleFactor), |
760 | }; |
761 | final Widget child = RepaintBoundary( |
762 | child: EditableText( |
763 | key: editableTextKey, |
764 | style: effectiveTextStyle, |
765 | readOnly: true, |
766 | toolbarOptions: widget.toolbarOptions, |
767 | textWidthBasis: widget.textWidthBasis ?? defaultTextStyle.textWidthBasis, |
768 | textHeightBehavior: widget.textHeightBehavior ?? defaultTextStyle.textHeightBehavior, |
769 | showSelectionHandles: _showSelectionHandles, |
770 | showCursor: widget.showCursor, |
771 | controller: _controller, |
772 | focusNode: focusNode, |
773 | strutStyle: widget.strutStyle ?? const StrutStyle(), |
774 | textAlign: widget.textAlign ?? defaultTextStyle.textAlign ?? TextAlign.start, |
775 | textDirection: widget.textDirection, |
776 | textScaler: effectiveScaler, |
777 | autofocus: widget.autofocus, |
778 | forceLine: false, |
779 | minLines: widget.minLines, |
780 | maxLines: widget.maxLines ?? defaultTextStyle.maxLines, |
781 | selectionColor: widget.selectionColor ?? selectionColor, |
782 | selectionControls: widget.selectionEnabled ? textSelectionControls : null, |
783 | onSelectionChanged: _handleSelectionChanged, |
784 | onSelectionHandleTapped: _handleSelectionHandleTapped, |
785 | rendererIgnoresPointer: true, |
786 | cursorWidth: widget.cursorWidth, |
787 | cursorHeight: widget.cursorHeight, |
788 | cursorRadius: cursorRadius, |
789 | cursorColor: cursorColor, |
790 | selectionHeightStyle: widget.selectionHeightStyle, |
791 | selectionWidthStyle: widget.selectionWidthStyle, |
792 | cursorOpacityAnimates: cursorOpacityAnimates, |
793 | cursorOffset: cursorOffset, |
794 | paintCursorAboveText: paintCursorAboveText, |
795 | backgroundCursorColor: CupertinoColors.inactiveGray, |
796 | enableInteractiveSelection: widget.enableInteractiveSelection, |
797 | magnifierConfiguration: |
798 | widget.magnifierConfiguration ?? TextMagnifier.adaptiveMagnifierConfiguration, |
799 | dragStartBehavior: widget.dragStartBehavior, |
800 | scrollPhysics: widget.scrollPhysics, |
801 | scrollBehavior: widget.scrollBehavior, |
802 | autofillHints: null, |
803 | contextMenuBuilder: widget.contextMenuBuilder, |
804 | ), |
805 | ); |
806 | |
807 | return Semantics( |
808 | label: widget.semanticsLabel, |
809 | excludeSemantics: widget.semanticsLabel != null, |
810 | onLongPress: () { |
811 | _effectiveFocusNode.requestFocus(); |
812 | }, |
813 | child: _selectionGestureDetectorBuilder.buildGestureDetector( |
814 | behavior: HitTestBehavior.translucent, |
815 | child: child, |
816 | ), |
817 | ); |
818 | } |
819 | } |
820 |
Definitions
- iOSHorizontalOffset
- _TextSpanEditingController
- _TextSpanEditingController
- buildTextSpan
- text
- _SelectableTextSelectionGestureDetectorBuilder
- _SelectableTextSelectionGestureDetectorBuilder
- onSingleTapUp
- SelectableText
- SelectableText
- rich
- selectionEnabled
- _defaultContextMenuBuilder
- createState
- debugFillProperties
- _SelectableTextState
- _editableText
- _effectiveFocusNode
- selectionEnabled
- initState
- didUpdateWidget
- dispose
- _onControllerChanged
- _handleFocusChanged
- _handleSelectionChanged
- _handleSelectionHandleTapped
- _shouldShowSelectionHandles
Learn more about Flutter for embedded and desktop on industrialflutter.com