| // Copyright 2014 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'package:flutter/cupertino.dart'; |
| import 'package:flutter/rendering.dart'; |
| import 'package:flutter/foundation.dart'; |
| import 'package:flutter/gestures.dart'; |
| |
| import 'desktop_text_selection.dart'; |
| import 'feedback.dart'; |
| import 'text_selection.dart'; |
| import 'text_selection_theme.dart'; |
| import 'theme.dart'; |
| |
| /// An eyeballed value that moves the cursor slightly left of where it is |
| /// rendered for text on Android so its positioning more accurately matches the |
| /// native iOS text cursor positioning. |
| /// |
| /// This value is in device pixels, not logical pixels as is typically used |
| /// throughout the codebase. |
| const int iOSHorizontalOffset = -2; |
| |
| class _TextSpanEditingController extends TextEditingController { |
| _TextSpanEditingController({required TextSpan textSpan}): |
| assert(textSpan != null), |
| _textSpan = textSpan, |
| super(text: textSpan.toPlainText(includeSemanticsLabels: false)); |
| |
| final TextSpan _textSpan; |
| |
| @override |
| TextSpan buildTextSpan({required BuildContext context, TextStyle? style, required bool withComposing}) { |
| // This does not care about composing. |
| return TextSpan( |
| style: style, |
| children: <TextSpan>[_textSpan], |
| ); |
| } |
| |
| @override |
| set text(String? newText) { |
| // This should never be reached. |
| throw UnimplementedError(); |
| } |
| } |
| |
| class _SelectableTextSelectionGestureDetectorBuilder extends TextSelectionGestureDetectorBuilder { |
| _SelectableTextSelectionGestureDetectorBuilder({ |
| required _SelectableTextState state, |
| }) : _state = state, |
| super(delegate: state); |
| |
| final _SelectableTextState _state; |
| |
| @override |
| void onForcePressStart(ForcePressDetails details) { |
| super.onForcePressStart(details); |
| if (delegate.selectionEnabled && shouldShowSelectionToolbar) { |
| editableText.showToolbar(); |
| } |
| } |
| |
| @override |
| void onForcePressEnd(ForcePressDetails details) { |
| // Not required. |
| } |
| |
| @override |
| void onSingleLongTapMoveUpdate(LongPressMoveUpdateDetails details) { |
| if (delegate.selectionEnabled) { |
| renderEditable.selectWordsInRange( |
| from: details.globalPosition - details.offsetFromOrigin, |
| to: details.globalPosition, |
| cause: SelectionChangedCause.longPress, |
| ); |
| } |
| } |
| |
| @override |
| void onSingleTapUp(TapUpDetails details) { |
| editableText.hideToolbar(); |
| if (delegate.selectionEnabled) { |
| switch (Theme.of(_state.context).platform) { |
| case TargetPlatform.iOS: |
| case TargetPlatform.macOS: |
| renderEditable.selectWordEdge(cause: SelectionChangedCause.tap); |
| break; |
| case TargetPlatform.android: |
| case TargetPlatform.fuchsia: |
| case TargetPlatform.linux: |
| case TargetPlatform.windows: |
| renderEditable.selectPosition(cause: SelectionChangedCause.tap); |
| break; |
| } |
| } |
| _state.widget.onTap?.call(); |
| } |
| |
| @override |
| void onSingleLongTapStart(LongPressStartDetails details) { |
| if (delegate.selectionEnabled) { |
| renderEditable.selectWord(cause: SelectionChangedCause.longPress); |
| Feedback.forLongPress(_state.context); |
| } |
| } |
| } |
| |
| /// A run of selectable text with a single style. |
| /// |
| /// The [SelectableText] widget displays a string of text with a single style. |
| /// The string might break across multiple lines or might all be displayed on |
| /// the same line depending on the layout constraints. |
| /// |
| /// {@youtube 560 315 https://www.youtube.com/watch?v=ZSU3ZXOs6hc} |
| /// |
| /// The [style] argument is optional. When omitted, the text will use the style |
| /// from the closest enclosing [DefaultTextStyle]. If the given style's |
| /// [TextStyle.inherit] property is true (the default), the given style will |
| /// be merged with the closest enclosing [DefaultTextStyle]. This merging |
| /// behavior is useful, for example, to make the text bold while using the |
| /// default font family and size. |
| /// |
| /// {@tool snippet} |
| /// |
| /// ```dart |
| /// const SelectableText( |
| /// 'Hello! How are you?', |
| /// textAlign: TextAlign.center, |
| /// style: TextStyle(fontWeight: FontWeight.bold), |
| /// ) |
| /// ``` |
| /// {@end-tool} |
| /// |
| /// Using the [SelectableText.rich] constructor, the [SelectableText] widget can |
| /// display a paragraph with differently styled [TextSpan]s. The sample |
| /// that follows displays "Hello beautiful world" with different styles |
| /// for each word. |
| /// |
| /// {@tool snippet} |
| /// |
| /// ```dart |
| /// const SelectableText.rich( |
| /// TextSpan( |
| /// text: 'Hello', // default text style |
| /// children: <TextSpan>[ |
| /// TextSpan(text: ' beautiful ', style: TextStyle(fontStyle: FontStyle.italic)), |
| /// TextSpan(text: 'world', style: TextStyle(fontWeight: FontWeight.bold)), |
| /// ], |
| /// ), |
| /// ) |
| /// ``` |
| /// {@end-tool} |
| /// |
| /// ## Interactivity |
| /// |
| /// To make [SelectableText] react to touch events, use callback [onTap] to achieve |
| /// the desired behavior. |
| /// |
| /// See also: |
| /// |
| /// * [Text], which is the non selectable version of this widget. |
| /// * [TextField], which is the editable version of this widget. |
| class SelectableText extends StatefulWidget { |
| /// Creates a selectable text widget. |
| /// |
| /// If the [style] argument is null, the text will use the style from the |
| /// closest enclosing [DefaultTextStyle]. |
| /// |
| |
| /// The [showCursor], [autofocus], [dragStartBehavior], and [data] parameters |
| /// must not be null. If specified, the [maxLines] argument must be greater |
| /// than zero. |
| const SelectableText( |
| String this.data, { |
| Key? key, |
| this.focusNode, |
| this.style, |
| this.strutStyle, |
| this.textAlign, |
| this.textDirection, |
| this.textScaleFactor, |
| this.showCursor = false, |
| this.autofocus = false, |
| ToolbarOptions? toolbarOptions, |
| this.minLines, |
| this.maxLines, |
| this.cursorWidth = 2.0, |
| this.cursorHeight, |
| this.cursorRadius, |
| this.cursorColor, |
| this.dragStartBehavior = DragStartBehavior.start, |
| this.enableInteractiveSelection = true, |
| this.selectionControls, |
| this.onTap, |
| this.scrollPhysics, |
| this.textHeightBehavior, |
| this.textWidthBasis, |
| this.onSelectionChanged, |
| }) : assert(showCursor != null), |
| assert(autofocus != null), |
| assert(dragStartBehavior != null), |
| assert(maxLines == null || maxLines > 0), |
| assert(minLines == null || minLines > 0), |
| assert( |
| (maxLines == null) || (minLines == null) || (maxLines >= minLines), |
| 'minLines can\'t be greater than maxLines', |
| ), |
| assert( |
| data != null, |
| 'A non-null String must be provided to a SelectableText widget.', |
| ), |
| textSpan = null, |
| toolbarOptions = toolbarOptions ?? |
| const ToolbarOptions( |
| selectAll: true, |
| copy: true, |
| ), |
| super(key: key); |
| |
| /// Creates a selectable text widget with a [TextSpan]. |
| /// |
| /// The [textSpan] parameter must not be null and only contain [TextSpan] in |
| /// [textSpan.children]. Other type of [InlineSpan] is not allowed. |
| /// |
| /// The [autofocus] and [dragStartBehavior] arguments must not be null. |
| const SelectableText.rich( |
| TextSpan this.textSpan, { |
| Key? key, |
| this.focusNode, |
| this.style, |
| this.strutStyle, |
| this.textAlign, |
| this.textDirection, |
| this.textScaleFactor, |
| this.showCursor = false, |
| this.autofocus = false, |
| ToolbarOptions? toolbarOptions, |
| this.minLines, |
| this.maxLines, |
| this.cursorWidth = 2.0, |
| this.cursorHeight, |
| this.cursorRadius, |
| this.cursorColor, |
| this.dragStartBehavior = DragStartBehavior.start, |
| this.enableInteractiveSelection = true, |
| this.selectionControls, |
| this.onTap, |
| this.scrollPhysics, |
| this.textHeightBehavior, |
| this.textWidthBasis, |
| this.onSelectionChanged, |
| }) : assert(showCursor != null), |
| assert(autofocus != null), |
| assert(dragStartBehavior != null), |
| assert(maxLines == null || maxLines > 0), |
| assert(minLines == null || minLines > 0), |
| assert( |
| (maxLines == null) || (minLines == null) || (maxLines >= minLines), |
| 'minLines can\'t be greater than maxLines', |
| ), |
| assert( |
| textSpan != null, |
| 'A non-null TextSpan must be provided to a SelectableText.rich widget.', |
| ), |
| data = null, |
| toolbarOptions = toolbarOptions ?? |
| const ToolbarOptions( |
| selectAll: true, |
| copy: true, |
| ), |
| super(key: key); |
| |
| /// The text to display. |
| /// |
| /// This will be null if a [textSpan] is provided instead. |
| final String? data; |
| |
| /// The text to display as a [TextSpan]. |
| /// |
| /// This will be null if [data] is provided instead. |
| final TextSpan? textSpan; |
| |
| /// Defines the focus for this widget. |
| /// |
| /// Text is only selectable when widget is focused. |
| /// |
| /// The [focusNode] is a long-lived object that's typically managed by a |
| /// [StatefulWidget] parent. See [FocusNode] for more information. |
| /// |
| /// To give the focus to this widget, provide a [focusNode] and then |
| /// use the current [FocusScope] to request the focus: |
| /// |
| /// ```dart |
| /// FocusScope.of(context).requestFocus(myFocusNode); |
| /// ``` |
| /// |
| /// This happens automatically when the widget is tapped. |
| /// |
| /// To be notified when the widget gains or loses the focus, add a listener |
| /// to the [focusNode]: |
| /// |
| /// ```dart |
| /// focusNode.addListener(() { print(myFocusNode.hasFocus); }); |
| /// ``` |
| /// |
| /// If null, this widget will create its own [FocusNode]. |
| final FocusNode? focusNode; |
| |
| /// The style to use for the text. |
| /// |
| /// If null, defaults [DefaultTextStyle] of context. |
| final TextStyle? style; |
| |
| /// {@macro flutter.widgets.editableText.strutStyle} |
| final StrutStyle? strutStyle; |
| |
| /// {@macro flutter.widgets.editableText.textAlign} |
| final TextAlign? textAlign; |
| |
| /// {@macro flutter.widgets.editableText.textDirection} |
| final TextDirection? textDirection; |
| |
| /// {@macro flutter.widgets.editableText.textScaleFactor} |
| final double? textScaleFactor; |
| |
| /// {@macro flutter.widgets.editableText.autofocus} |
| final bool autofocus; |
| |
| /// {@macro flutter.widgets.editableText.minLines} |
| final int? minLines; |
| |
| /// {@macro flutter.widgets.editableText.maxLines} |
| final int? maxLines; |
| |
| /// {@macro flutter.widgets.editableText.showCursor} |
| final bool showCursor; |
| |
| /// {@macro flutter.widgets.editableText.cursorWidth} |
| final double cursorWidth; |
| |
| /// {@macro flutter.widgets.editableText.cursorHeight} |
| final double? cursorHeight; |
| |
| /// {@macro flutter.widgets.editableText.cursorRadius} |
| final Radius? cursorRadius; |
| |
| /// The color to use when painting the cursor. |
| /// |
| /// Defaults to the theme's `cursorColor` when null. |
| final Color? cursorColor; |
| |
| /// {@macro flutter.widgets.editableText.enableInteractiveSelection} |
| final bool enableInteractiveSelection; |
| |
| /// {@macro flutter.widgets.editableText.selectionControls} |
| final TextSelectionControls? selectionControls; |
| |
| /// {@macro flutter.widgets.scrollable.dragStartBehavior} |
| final DragStartBehavior dragStartBehavior; |
| |
| /// Configuration of toolbar options. |
| /// |
| /// Paste and cut will be disabled regardless. |
| /// |
| /// If not set, select all and copy will be enabled by default. |
| final ToolbarOptions toolbarOptions; |
| |
| /// {@macro flutter.widgets.editableText.selectionEnabled} |
| bool get selectionEnabled => enableInteractiveSelection; |
| |
| /// Called when the user taps on this selectable text. |
| /// |
| /// The selectable text builds a [GestureDetector] to handle input events like tap, |
| /// to trigger focus requests, to move the caret, adjust the selection, etc. |
| /// Handling some of those events by wrapping the selectable text with a competing |
| /// GestureDetector is problematic. |
| /// |
| /// To unconditionally handle taps, without interfering with the selectable text's |
| /// internal gesture detector, provide this callback. |
| /// |
| /// To be notified when the text field gains or loses the focus, provide a |
| /// [focusNode] and add a listener to that. |
| /// |
| /// To listen to arbitrary pointer events without competing with the |
| /// selectable text's internal gesture detector, use a [Listener]. |
| final GestureTapCallback? onTap; |
| |
| /// {@macro flutter.widgets.editableText.scrollPhysics} |
| final ScrollPhysics? scrollPhysics; |
| |
| /// {@macro flutter.dart:ui.textHeightBehavior} |
| final TextHeightBehavior? textHeightBehavior; |
| |
| /// {@macro flutter.painting.textPainter.textWidthBasis} |
| final TextWidthBasis? textWidthBasis; |
| |
| /// {@macro flutter.widgets.editableText.onSelectionChanged} |
| final SelectionChangedCallback? onSelectionChanged; |
| |
| @override |
| _SelectableTextState createState() => _SelectableTextState(); |
| |
| @override |
| void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
| super.debugFillProperties(properties); |
| properties.add(DiagnosticsProperty<String>('data', data, defaultValue: null)); |
| properties.add(DiagnosticsProperty<FocusNode>('focusNode', focusNode, defaultValue: null)); |
| properties.add(DiagnosticsProperty<TextStyle>('style', style, defaultValue: null)); |
| properties.add(DiagnosticsProperty<bool>('autofocus', autofocus, defaultValue: false)); |
| properties.add(DiagnosticsProperty<bool>('showCursor', showCursor, defaultValue: false)); |
| properties.add(IntProperty('minLines', minLines, defaultValue: null)); |
| properties.add(IntProperty('maxLines', maxLines, defaultValue: null)); |
| properties.add(EnumProperty<TextAlign>('textAlign', textAlign, defaultValue: null)); |
| properties.add(EnumProperty<TextDirection>('textDirection', textDirection, defaultValue: null)); |
| properties.add(DoubleProperty('textScaleFactor', textScaleFactor, defaultValue: null)); |
| properties.add(DoubleProperty('cursorWidth', cursorWidth, defaultValue: 2.0)); |
| properties.add(DoubleProperty('cursorHeight', cursorHeight, defaultValue: null)); |
| properties.add(DiagnosticsProperty<Radius>('cursorRadius', cursorRadius, defaultValue: null)); |
| properties.add(DiagnosticsProperty<Color>('cursorColor', cursorColor, defaultValue: null)); |
| properties.add(FlagProperty('selectionEnabled', value: selectionEnabled, defaultValue: true, ifFalse: 'selection disabled')); |
| properties.add(DiagnosticsProperty<TextSelectionControls>('selectionControls', selectionControls, defaultValue: null)); |
| properties.add(DiagnosticsProperty<ScrollPhysics>('scrollPhysics', scrollPhysics, defaultValue: null)); |
| properties.add(DiagnosticsProperty<TextHeightBehavior>('textHeightBehavior', textHeightBehavior, defaultValue: null)); |
| } |
| } |
| |
| class _SelectableTextState extends State<SelectableText> with AutomaticKeepAliveClientMixin implements TextSelectionGestureDetectorBuilderDelegate { |
| EditableTextState? get _editableText => editableTextKey.currentState; |
| |
| late _TextSpanEditingController _controller; |
| |
| FocusNode? _focusNode; |
| FocusNode get _effectiveFocusNode => widget.focusNode ?? (_focusNode ??= FocusNode()); |
| |
| bool _showSelectionHandles = false; |
| |
| late _SelectableTextSelectionGestureDetectorBuilder _selectionGestureDetectorBuilder; |
| |
| // API for TextSelectionGestureDetectorBuilderDelegate. |
| @override |
| late bool forcePressEnabled; |
| |
| @override |
| final GlobalKey<EditableTextState> editableTextKey = GlobalKey<EditableTextState>(); |
| |
| @override |
| bool get selectionEnabled => widget.selectionEnabled; |
| // End of API for TextSelectionGestureDetectorBuilderDelegate. |
| |
| @override |
| void initState() { |
| super.initState(); |
| _selectionGestureDetectorBuilder = _SelectableTextSelectionGestureDetectorBuilder(state: this); |
| _controller = _TextSpanEditingController( |
| textSpan: widget.textSpan ?? TextSpan(text: widget.data) |
| ); |
| _controller.addListener(_onControllerChanged); |
| } |
| |
| @override |
| void didUpdateWidget(SelectableText oldWidget) { |
| super.didUpdateWidget(oldWidget); |
| if (widget.data != oldWidget.data || widget.textSpan != oldWidget.textSpan) { |
| _controller.removeListener(_onControllerChanged); |
| _controller = _TextSpanEditingController( |
| textSpan: widget.textSpan ?? TextSpan(text: widget.data) |
| ); |
| _controller.addListener(_onControllerChanged); |
| } |
| if (_effectiveFocusNode.hasFocus && _controller.selection.isCollapsed) { |
| _showSelectionHandles = false; |
| } else { |
| _showSelectionHandles = true; |
| } |
| } |
| |
| @override |
| void dispose() { |
| _focusNode?.dispose(); |
| _controller.removeListener(_onControllerChanged); |
| super.dispose(); |
| } |
| |
| void _onControllerChanged() { |
| final bool showSelectionHandles = !_effectiveFocusNode.hasFocus |
| || !_controller.selection.isCollapsed; |
| if (showSelectionHandles == _showSelectionHandles) { |
| return; |
| } |
| setState(() { |
| _showSelectionHandles = showSelectionHandles; |
| }); |
| } |
| |
| TextSelection? _lastSeenTextSelection; |
| |
| void _handleSelectionChanged(TextSelection selection, SelectionChangedCause? cause) { |
| final bool willShowSelectionHandles = _shouldShowSelectionHandles(cause); |
| if (willShowSelectionHandles != _showSelectionHandles) { |
| setState(() { |
| _showSelectionHandles = willShowSelectionHandles; |
| }); |
| } |
| // TODO(chunhtai): The selection may be the same. We should remove this |
| // check once this is fixed https://github.com/flutter/flutter/issues/76349. |
| if (widget.onSelectionChanged != null && _lastSeenTextSelection != selection) { |
| widget.onSelectionChanged!(selection, cause); |
| } |
| _lastSeenTextSelection = selection; |
| |
| switch (Theme.of(context).platform) { |
| case TargetPlatform.iOS: |
| case TargetPlatform.macOS: |
| if (cause == SelectionChangedCause.longPress) { |
| _editableText?.bringIntoView(selection.base); |
| } |
| return; |
| case TargetPlatform.android: |
| case TargetPlatform.fuchsia: |
| case TargetPlatform.linux: |
| case TargetPlatform.windows: |
| // Do nothing. |
| } |
| } |
| |
| /// Toggle the toolbar when a selection handle is tapped. |
| void _handleSelectionHandleTapped() { |
| if (_controller.selection.isCollapsed) { |
| _editableText!.toggleToolbar(); |
| } |
| } |
| |
| bool _shouldShowSelectionHandles(SelectionChangedCause? cause) { |
| // When the text field is activated by something that doesn't trigger the |
| // selection overlay, we shouldn't show the handles either. |
| if (!_selectionGestureDetectorBuilder.shouldShowSelectionToolbar) |
| return false; |
| |
| if (_controller.selection.isCollapsed) |
| return false; |
| |
| if (cause == SelectionChangedCause.keyboard) |
| return false; |
| |
| if (cause == SelectionChangedCause.longPress) |
| return true; |
| |
| if (_controller.text.isNotEmpty) |
| return true; |
| |
| return false; |
| } |
| |
| @override |
| bool get wantKeepAlive => true; |
| |
| @override |
| Widget build(BuildContext context) { |
| super.build(context); // See AutomaticKeepAliveClientMixin. |
| assert(() { |
| return _controller._textSpan.visitChildren((InlineSpan span) => span.runtimeType == TextSpan); |
| }(), 'SelectableText only supports TextSpan; Other type of InlineSpan is not allowed'); |
| assert(debugCheckHasMediaQuery(context)); |
| assert(debugCheckHasDirectionality(context)); |
| assert( |
| !(widget.style != null && widget.style!.inherit == false && |
| (widget.style!.fontSize == null || widget.style!.textBaseline == null)), |
| 'inherit false style must supply fontSize and textBaseline', |
| ); |
| |
| final ThemeData theme = Theme.of(context); |
| final TextSelectionThemeData selectionTheme = TextSelectionTheme.of(context); |
| final FocusNode focusNode = _effectiveFocusNode; |
| |
| TextSelectionControls? textSelectionControls = widget.selectionControls; |
| final bool paintCursorAboveText; |
| final bool cursorOpacityAnimates; |
| Offset? cursorOffset; |
| Color? cursorColor = widget.cursorColor; |
| final Color selectionColor; |
| Radius? cursorRadius = widget.cursorRadius; |
| |
| switch (theme.platform) { |
| case TargetPlatform.iOS: |
| final CupertinoThemeData cupertinoTheme = CupertinoTheme.of(context); |
| forcePressEnabled = true; |
| textSelectionControls ??= cupertinoTextSelectionControls; |
| paintCursorAboveText = true; |
| cursorOpacityAnimates = true; |
| cursorColor ??= selectionTheme.cursorColor ?? cupertinoTheme.primaryColor; |
| selectionColor = selectionTheme.selectionColor ?? cupertinoTheme.primaryColor.withOpacity(0.40); |
| cursorRadius ??= const Radius.circular(2.0); |
| cursorOffset = Offset(iOSHorizontalOffset / MediaQuery.of(context).devicePixelRatio, 0); |
| break; |
| |
| case TargetPlatform.macOS: |
| final CupertinoThemeData cupertinoTheme = CupertinoTheme.of(context); |
| forcePressEnabled = false; |
| textSelectionControls ??= cupertinoDesktopTextSelectionControls; |
| paintCursorAboveText = true; |
| cursorOpacityAnimates = true; |
| cursorColor ??= selectionTheme.cursorColor ?? cupertinoTheme.primaryColor; |
| selectionColor = selectionTheme.selectionColor ?? cupertinoTheme.primaryColor.withOpacity(0.40); |
| cursorRadius ??= const Radius.circular(2.0); |
| cursorOffset = Offset(iOSHorizontalOffset / MediaQuery.of(context).devicePixelRatio, 0); |
| break; |
| |
| case TargetPlatform.android: |
| case TargetPlatform.fuchsia: |
| forcePressEnabled = false; |
| textSelectionControls ??= materialTextSelectionControls; |
| paintCursorAboveText = false; |
| cursorOpacityAnimates = false; |
| cursorColor ??= selectionTheme.cursorColor ?? theme.colorScheme.primary; |
| selectionColor = selectionTheme.selectionColor ?? theme.colorScheme.primary.withOpacity(0.40); |
| break; |
| |
| case TargetPlatform.linux: |
| case TargetPlatform.windows: |
| forcePressEnabled = false; |
| textSelectionControls ??= desktopTextSelectionControls; |
| paintCursorAboveText = false; |
| cursorOpacityAnimates = false; |
| cursorColor ??= selectionTheme.cursorColor ?? theme.colorScheme.primary; |
| selectionColor = selectionTheme.selectionColor ?? theme.colorScheme.primary.withOpacity(0.40); |
| break; |
| } |
| |
| final DefaultTextStyle defaultTextStyle = DefaultTextStyle.of(context); |
| TextStyle? effectiveTextStyle = widget.style; |
| if (effectiveTextStyle == null || effectiveTextStyle.inherit) |
| effectiveTextStyle = defaultTextStyle.style.merge(widget.style); |
| if (MediaQuery.boldTextOverride(context)) |
| effectiveTextStyle = effectiveTextStyle.merge(const TextStyle(fontWeight: FontWeight.bold)); |
| final Widget child = RepaintBoundary( |
| child: EditableText( |
| key: editableTextKey, |
| style: effectiveTextStyle, |
| readOnly: true, |
| textWidthBasis: widget.textWidthBasis ?? defaultTextStyle.textWidthBasis, |
| textHeightBehavior: widget.textHeightBehavior ?? defaultTextStyle.textHeightBehavior, |
| showSelectionHandles: _showSelectionHandles, |
| showCursor: widget.showCursor, |
| controller: _controller, |
| focusNode: focusNode, |
| strutStyle: widget.strutStyle ?? const StrutStyle(), |
| textAlign: widget.textAlign ?? defaultTextStyle.textAlign ?? TextAlign.start, |
| textDirection: widget.textDirection, |
| textScaleFactor: widget.textScaleFactor, |
| autofocus: widget.autofocus, |
| forceLine: false, |
| toolbarOptions: widget.toolbarOptions, |
| minLines: widget.minLines, |
| maxLines: widget.maxLines ?? defaultTextStyle.maxLines, |
| selectionColor: selectionColor, |
| selectionControls: widget.selectionEnabled ? textSelectionControls : null, |
| onSelectionChanged: _handleSelectionChanged, |
| onSelectionHandleTapped: _handleSelectionHandleTapped, |
| rendererIgnoresPointer: true, |
| cursorWidth: widget.cursorWidth, |
| cursorHeight: widget.cursorHeight, |
| cursorRadius: cursorRadius, |
| cursorColor: cursorColor, |
| cursorOpacityAnimates: cursorOpacityAnimates, |
| cursorOffset: cursorOffset, |
| paintCursorAboveText: paintCursorAboveText, |
| backgroundCursorColor: CupertinoColors.inactiveGray, |
| enableInteractiveSelection: widget.enableInteractiveSelection, |
| dragStartBehavior: widget.dragStartBehavior, |
| scrollPhysics: widget.scrollPhysics, |
| ), |
| ); |
| |
| return Semantics( |
| onLongPress: () { |
| _effectiveFocusNode.requestFocus(); |
| }, |
| child: _selectionGestureDetectorBuilder.buildGestureDetector( |
| behavior: HitTestBehavior.translucent, |
| child: child, |
| ), |
| ); |
| } |
| } |