implement selectable text (#34019)
This commit is contained in:
parent
41bc10fa70
commit
2338576aa6
@ -93,6 +93,7 @@ export 'src/material/reorderable_list.dart';
|
|||||||
export 'src/material/scaffold.dart';
|
export 'src/material/scaffold.dart';
|
||||||
export 'src/material/scrollbar.dart';
|
export 'src/material/scrollbar.dart';
|
||||||
export 'src/material/search.dart';
|
export 'src/material/search.dart';
|
||||||
|
export 'src/material/selectable_text.dart';
|
||||||
export 'src/material/shadows.dart';
|
export 'src/material/shadows.dart';
|
||||||
export 'src/material/slider.dart';
|
export 'src/material/slider.dart';
|
||||||
export 'src/material/slider_theme.dart';
|
export 'src/material/slider_theme.dart';
|
||||||
|
580
packages/flutter/lib/src/material/selectable_text.dart
Normal file
580
packages/flutter/lib/src/material/selectable_text.dart
Normal file
@ -0,0 +1,580 @@
|
|||||||
|
// Copyright 2019 The Chromium 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/services.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/gestures.dart';
|
||||||
|
|
||||||
|
import 'feedback.dart';
|
||||||
|
import 'text_selection.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());
|
||||||
|
|
||||||
|
final TextSpan _textSpan;
|
||||||
|
|
||||||
|
@override
|
||||||
|
TextSpan buildTextSpan({TextStyle style ,bool withComposing}) {
|
||||||
|
// TODO(chunhtai): Implement composing.
|
||||||
|
return TextSpan(
|
||||||
|
style: style,
|
||||||
|
children: <TextSpan>[_textSpan],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
set text(String newText) {
|
||||||
|
// TODO(chunhtai): Implement value editing.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
switch (Theme.of(_state.context).platform) {
|
||||||
|
case TargetPlatform.iOS:
|
||||||
|
renderEditable.selectPositionAt(
|
||||||
|
from: details.globalPosition,
|
||||||
|
cause: SelectionChangedCause.longPress,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case TargetPlatform.android:
|
||||||
|
case TargetPlatform.fuchsia:
|
||||||
|
renderEditable.selectWordsInRange(
|
||||||
|
from: details.globalPosition - details.offsetFromOrigin,
|
||||||
|
to: details.globalPosition,
|
||||||
|
cause: SelectionChangedCause.longPress,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onSingleTapUp(TapUpDetails details) {
|
||||||
|
editableText.hideToolbar();
|
||||||
|
if (delegate.selectionEnabled) {
|
||||||
|
switch (Theme.of(_state.context).platform) {
|
||||||
|
case TargetPlatform.iOS:
|
||||||
|
renderEditable.selectWordEdge(cause: SelectionChangedCause.tap);
|
||||||
|
break;
|
||||||
|
case TargetPlatform.android:
|
||||||
|
case TargetPlatform.fuchsia:
|
||||||
|
renderEditable.selectPosition(cause: SelectionChangedCause.tap);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (_state.widget.onTap != null)
|
||||||
|
_state.widget.onTap();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onSingleLongTapStart(LongPressStartDetails details) {
|
||||||
|
if (delegate.selectionEnabled) {
|
||||||
|
switch (Theme.of(_state.context).platform) {
|
||||||
|
case TargetPlatform.iOS:
|
||||||
|
renderEditable.selectPositionAt(
|
||||||
|
from: details.globalPosition,
|
||||||
|
cause: SelectionChangedCause.longPress,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case TargetPlatform.android:
|
||||||
|
case TargetPlatform.fuchsia:
|
||||||
|
renderEditable.selectWord(cause: SelectionChangedCause.longPress);
|
||||||
|
Feedback.forLongPress(_state.context);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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.
|
||||||
|
///
|
||||||
|
/// 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 sample}
|
||||||
|
///
|
||||||
|
/// ```dart
|
||||||
|
/// 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 sample}
|
||||||
|
///
|
||||||
|
/// ```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 [data] parameter must not be null.
|
||||||
|
const SelectableText(
|
||||||
|
this.data, {
|
||||||
|
Key key,
|
||||||
|
this.focusNode,
|
||||||
|
this.style,
|
||||||
|
this.strutStyle,
|
||||||
|
this.textAlign,
|
||||||
|
this.textDirection,
|
||||||
|
this.showCursor = false,
|
||||||
|
this.autofocus = false,
|
||||||
|
this.maxLines,
|
||||||
|
this.cursorWidth = 2.0,
|
||||||
|
this.cursorRadius,
|
||||||
|
this.cursorColor,
|
||||||
|
this.dragStartBehavior = DragStartBehavior.start,
|
||||||
|
this.enableInteractiveSelection = true,
|
||||||
|
this.onTap,
|
||||||
|
this.scrollPhysics,
|
||||||
|
this.textWidthBasis,
|
||||||
|
}) : assert(showCursor != null),
|
||||||
|
assert(autofocus != null),
|
||||||
|
assert(dragStartBehavior != null),
|
||||||
|
assert(maxLines == null || maxLines > 0),
|
||||||
|
assert(
|
||||||
|
data != null,
|
||||||
|
'A non-null String must be provided to a SelectableText widget.',
|
||||||
|
),
|
||||||
|
textSpan = null,
|
||||||
|
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.
|
||||||
|
const SelectableText.rich(
|
||||||
|
this.textSpan, {
|
||||||
|
Key key,
|
||||||
|
this.focusNode,
|
||||||
|
this.style,
|
||||||
|
this.strutStyle,
|
||||||
|
this.textAlign,
|
||||||
|
this.textDirection,
|
||||||
|
this.showCursor = false,
|
||||||
|
this.autofocus = false,
|
||||||
|
this.maxLines,
|
||||||
|
this.cursorWidth = 2.0,
|
||||||
|
this.cursorRadius,
|
||||||
|
this.cursorColor,
|
||||||
|
this.dragStartBehavior = DragStartBehavior.start,
|
||||||
|
this.enableInteractiveSelection = true,
|
||||||
|
this.onTap,
|
||||||
|
this.scrollPhysics,
|
||||||
|
this.textWidthBasis,
|
||||||
|
}) : assert(showCursor != null),
|
||||||
|
assert(autofocus != null),
|
||||||
|
assert(dragStartBehavior != null),
|
||||||
|
assert(maxLines == null || maxLines > 0),
|
||||||
|
assert(
|
||||||
|
textSpan != null,
|
||||||
|
'A non-null TextSpan must be provided to a SelectableText.rich widget.',
|
||||||
|
),
|
||||||
|
data = null,
|
||||||
|
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.autofocus}
|
||||||
|
final bool autofocus;
|
||||||
|
|
||||||
|
/// {@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.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.scrollable.dragStartBehavior}
|
||||||
|
final DragStartBehavior dragStartBehavior;
|
||||||
|
|
||||||
|
/// {@macro flutter.rendering.editable.selectionEnabled}
|
||||||
|
bool get selectionEnabled {
|
||||||
|
return 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.edtiableText.scrollPhysics}
|
||||||
|
final ScrollPhysics scrollPhysics;
|
||||||
|
|
||||||
|
/// {@macro flutter.dart:ui.text.TextWidthBasis}
|
||||||
|
final TextWidthBasis textWidthBasis;
|
||||||
|
|
||||||
|
@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('maxLines', maxLines, defaultValue: null));
|
||||||
|
properties.add(EnumProperty<TextAlign>('textAlign', textAlign, defaultValue: null));
|
||||||
|
properties.add(EnumProperty<TextDirection>('textDirection', textDirection, defaultValue: null));
|
||||||
|
properties.add(DoubleProperty('cursorWidth', cursorWidth, defaultValue: 2.0));
|
||||||
|
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<ScrollPhysics>('scrollPhysics', scrollPhysics, defaultValue: null));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SelectableTextState extends State<SelectableText> with AutomaticKeepAliveClientMixin implements TextSelectionGestureDetectorBuilderDelegate {
|
||||||
|
EditableTextState get _editableText => editableTextKey.currentState;
|
||||||
|
|
||||||
|
_TextSpanEditingController _controller;
|
||||||
|
|
||||||
|
FocusNode _focusNode;
|
||||||
|
FocusNode get _effectiveFocusNode => widget.focusNode ?? (_focusNode ??= FocusNode());
|
||||||
|
|
||||||
|
bool _showSelectionHandles = false;
|
||||||
|
|
||||||
|
_SelectableTextSelectionGestureDetectorBuilder _selectionGestureDetectorBuilder;
|
||||||
|
|
||||||
|
// API for TextSelectionGestureDetectorBuilderDelegate.
|
||||||
|
@override
|
||||||
|
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)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(SelectableText oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
if (widget.data != oldWidget.data || widget.textSpan != oldWidget.textSpan) {
|
||||||
|
_controller = _TextSpanEditingController(
|
||||||
|
textSpan: widget.textSpan ?? TextSpan(text: widget.data)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (_effectiveFocusNode.hasFocus && _controller.selection.isCollapsed) {
|
||||||
|
_showSelectionHandles = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_focusNode?.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleSelectionChanged(TextSelection selection, SelectionChangedCause cause) {
|
||||||
|
final bool willShowSelectionHandles = _shouldShowSelectionHandles(cause);
|
||||||
|
if (willShowSelectionHandles != _showSelectionHandles) {
|
||||||
|
setState(() {
|
||||||
|
_showSelectionHandles = willShowSelectionHandles;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (Theme.of(context).platform) {
|
||||||
|
case TargetPlatform.iOS:
|
||||||
|
if (cause == SelectionChangedCause.longPress) {
|
||||||
|
_editableText?.bringIntoView(selection.base);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
case TargetPlatform.android:
|
||||||
|
case TargetPlatform.fuchsia:
|
||||||
|
// 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 themeData = Theme.of(context);
|
||||||
|
final FocusNode focusNode = _effectiveFocusNode;
|
||||||
|
|
||||||
|
TextSelectionControls textSelectionControls;
|
||||||
|
bool paintCursorAboveText;
|
||||||
|
bool cursorOpacityAnimates;
|
||||||
|
Offset cursorOffset;
|
||||||
|
Color cursorColor = widget.cursorColor;
|
||||||
|
Radius cursorRadius = widget.cursorRadius;
|
||||||
|
|
||||||
|
switch (themeData.platform) {
|
||||||
|
case TargetPlatform.iOS:
|
||||||
|
forcePressEnabled = true;
|
||||||
|
textSelectionControls = cupertinoTextSelectionControls;
|
||||||
|
paintCursorAboveText = true;
|
||||||
|
cursorOpacityAnimates = true;
|
||||||
|
cursorColor ??= CupertinoTheme.of(context).primaryColor;
|
||||||
|
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 ??= themeData.cursorColor;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
final DefaultTextStyle defaultTextStyle = DefaultTextStyle.of(context);
|
||||||
|
TextStyle effectiveTextStyle = widget.style;
|
||||||
|
if (widget.style == null || widget.style.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,
|
||||||
|
showSelectionHandles: _showSelectionHandles,
|
||||||
|
showCursor: widget.showCursor,
|
||||||
|
controller: _controller,
|
||||||
|
focusNode: focusNode,
|
||||||
|
strutStyle: widget.strutStyle ?? StrutStyle.disabled,
|
||||||
|
textAlign: widget.textAlign ?? defaultTextStyle.textAlign ?? TextAlign.start,
|
||||||
|
textDirection: widget.textDirection,
|
||||||
|
autofocus: widget.autofocus,
|
||||||
|
forceLine: false,
|
||||||
|
maxLines: widget.maxLines ?? defaultTextStyle.maxLines,
|
||||||
|
selectionColor: themeData.textSelectionColor,
|
||||||
|
selectionControls: widget.selectionEnabled ? textSelectionControls : null,
|
||||||
|
onSelectionChanged: _handleSelectionChanged,
|
||||||
|
onSelectionHandleTapped: _handleSelectionHandleTapped,
|
||||||
|
rendererIgnoresPointer: true,
|
||||||
|
cursorWidth: widget.cursorWidth,
|
||||||
|
cursorRadius: cursorRadius,
|
||||||
|
cursorColor: cursorColor,
|
||||||
|
cursorOpacityAnimates: cursorOpacityAnimates,
|
||||||
|
cursorOffset: cursorOffset,
|
||||||
|
paintCursorAboveText: paintCursorAboveText,
|
||||||
|
backgroundCursorColor: CupertinoColors.inactiveGray,
|
||||||
|
enableInteractiveSelection: widget.enableInteractiveSelection,
|
||||||
|
dragStartBehavior: widget.dragStartBehavior,
|
||||||
|
scrollPhysics: widget.scrollPhysics,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return Semantics(
|
||||||
|
onTap: () {
|
||||||
|
if (!_controller.selection.isValid)
|
||||||
|
_controller.selection = TextSelection.collapsed(offset: _controller.text.length);
|
||||||
|
_effectiveFocusNode.requestFocus();
|
||||||
|
},
|
||||||
|
onLongPress: () {
|
||||||
|
_effectiveFocusNode.requestFocus();
|
||||||
|
},
|
||||||
|
child: _selectionGestureDetectorBuilder.buildGestureDetector(
|
||||||
|
behavior: HitTestBehavior.translucent,
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -17,6 +17,7 @@ import 'ink_well.dart' show InteractiveInkFeature;
|
|||||||
import 'input_decorator.dart';
|
import 'input_decorator.dart';
|
||||||
import 'material.dart';
|
import 'material.dart';
|
||||||
import 'material_localizations.dart';
|
import 'material_localizations.dart';
|
||||||
|
import 'selectable_text.dart' show iOSHorizontalOffset;
|
||||||
import 'text_selection.dart';
|
import 'text_selection.dart';
|
||||||
import 'theme.dart';
|
import 'theme.dart';
|
||||||
|
|
||||||
@ -932,14 +933,7 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
|
|||||||
cursorOpacityAnimates = true;
|
cursorOpacityAnimates = true;
|
||||||
cursorColor ??= CupertinoTheme.of(context).primaryColor;
|
cursorColor ??= CupertinoTheme.of(context).primaryColor;
|
||||||
cursorRadius ??= const Radius.circular(2.0);
|
cursorRadius ??= const Radius.circular(2.0);
|
||||||
// An eyeballed value that moves the cursor slightly left of where it is
|
cursorOffset = Offset(iOSHorizontalOffset / MediaQuery.of(context).devicePixelRatio, 0);
|
||||||
// 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;
|
|
||||||
cursorOffset = Offset(_iOSHorizontalOffset / MediaQuery.of(context).devicePixelRatio, 0);
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case TargetPlatform.android:
|
case TargetPlatform.android:
|
||||||
|
@ -652,7 +652,7 @@ class TextPainter {
|
|||||||
|
|
||||||
final double caretEnd = box.end;
|
final double caretEnd = box.end;
|
||||||
final double dx = box.direction == TextDirection.rtl ? caretEnd - caretPrototype.width : caretEnd;
|
final double dx = box.direction == TextDirection.rtl ? caretEnd - caretPrototype.width : caretEnd;
|
||||||
return Rect.fromLTRB(min(dx, width), box.top, min(dx, width), box.bottom);
|
return Rect.fromLTRB(min(dx, _paragraph.width), box.top, min(dx, _paragraph.width), box.bottom);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -694,7 +694,7 @@ class TextPainter {
|
|||||||
final TextBox box = boxes.last;
|
final TextBox box = boxes.last;
|
||||||
final double caretStart = box.start;
|
final double caretStart = box.start;
|
||||||
final double dx = box.direction == TextDirection.rtl ? caretStart - caretPrototype.width : caretStart;
|
final double dx = box.direction == TextDirection.rtl ? caretStart - caretPrototype.width : caretStart;
|
||||||
return Rect.fromLTRB(min(dx, width), box.top, min(dx, width), box.bottom);
|
return Rect.fromLTRB(min(dx, _paragraph.width), box.top, min(dx, _paragraph.width), box.bottom);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -157,6 +157,9 @@ class RenderEditable extends RenderBox {
|
|||||||
this.onSelectionChanged,
|
this.onSelectionChanged,
|
||||||
this.onCaretChanged,
|
this.onCaretChanged,
|
||||||
this.ignorePointer = false,
|
this.ignorePointer = false,
|
||||||
|
bool readOnly = false,
|
||||||
|
bool forceLine = true,
|
||||||
|
TextWidthBasis textWidthBasis = TextWidthBasis.parent,
|
||||||
bool obscureText = false,
|
bool obscureText = false,
|
||||||
Locale locale,
|
Locale locale,
|
||||||
double cursorWidth = 1.0,
|
double cursorWidth = 1.0,
|
||||||
@ -185,11 +188,14 @@ class RenderEditable extends RenderBox {
|
|||||||
assert(textScaleFactor != null),
|
assert(textScaleFactor != null),
|
||||||
assert(offset != null),
|
assert(offset != null),
|
||||||
assert(ignorePointer != null),
|
assert(ignorePointer != null),
|
||||||
|
assert(textWidthBasis != null),
|
||||||
assert(paintCursorAboveText != null),
|
assert(paintCursorAboveText != null),
|
||||||
assert(obscureText != null),
|
assert(obscureText != null),
|
||||||
assert(textSelectionDelegate != null),
|
assert(textSelectionDelegate != null),
|
||||||
assert(cursorWidth != null && cursorWidth >= 0.0),
|
assert(cursorWidth != null && cursorWidth >= 0.0),
|
||||||
assert(devicePixelRatio != null),
|
assert(readOnly != null),
|
||||||
|
assert(forceLine != null),
|
||||||
|
assert(devicePixelRatio != null),
|
||||||
_textPainter = TextPainter(
|
_textPainter = TextPainter(
|
||||||
text: text,
|
text: text,
|
||||||
textAlign: textAlign,
|
textAlign: textAlign,
|
||||||
@ -197,6 +203,7 @@ class RenderEditable extends RenderBox {
|
|||||||
textScaleFactor: textScaleFactor,
|
textScaleFactor: textScaleFactor,
|
||||||
locale: locale,
|
locale: locale,
|
||||||
strutStyle: strutStyle,
|
strutStyle: strutStyle,
|
||||||
|
textWidthBasis: textWidthBasis,
|
||||||
),
|
),
|
||||||
_cursorColor = cursorColor,
|
_cursorColor = cursorColor,
|
||||||
_backgroundCursorColor = backgroundCursorColor,
|
_backgroundCursorColor = backgroundCursorColor,
|
||||||
@ -216,7 +223,9 @@ class RenderEditable extends RenderBox {
|
|||||||
_devicePixelRatio = devicePixelRatio,
|
_devicePixelRatio = devicePixelRatio,
|
||||||
_startHandleLayerLink = startHandleLayerLink,
|
_startHandleLayerLink = startHandleLayerLink,
|
||||||
_endHandleLayerLink = endHandleLayerLink,
|
_endHandleLayerLink = endHandleLayerLink,
|
||||||
_obscureText = obscureText {
|
_obscureText = obscureText,
|
||||||
|
_readOnly = readOnly,
|
||||||
|
_forceLine = forceLine {
|
||||||
assert(_showCursor != null);
|
assert(_showCursor != null);
|
||||||
assert(!_showCursor.value || cursorColor != null);
|
assert(!_showCursor.value || cursorColor != null);
|
||||||
this.hasFocus = hasFocus ?? false;
|
this.hasFocus = hasFocus ?? false;
|
||||||
@ -245,12 +254,15 @@ class RenderEditable extends RenderBox {
|
|||||||
/// The default value of this property is false.
|
/// The default value of this property is false.
|
||||||
bool ignorePointer;
|
bool ignorePointer;
|
||||||
|
|
||||||
/// Whether text is composed.
|
/// {@macro flutter.widgets.text.DefaultTextStyle.textWidthBasis}
|
||||||
///
|
TextWidthBasis get textWidthBasis => _textPainter.textWidthBasis;
|
||||||
/// Text is composed when user selects it for editing. The [TextSpan] will have
|
set textWidthBasis(TextWidthBasis value) {
|
||||||
/// children with composing effect and leave text property to be null.
|
assert(value != null);
|
||||||
@visibleForTesting
|
if (_textPainter.textWidthBasis == value)
|
||||||
bool get isComposingText => text.text == null;
|
return;
|
||||||
|
_textPainter.textWidthBasis = value;
|
||||||
|
markNeedsTextLayout();
|
||||||
|
}
|
||||||
|
|
||||||
/// The pixel ratio of the current device.
|
/// The pixel ratio of the current device.
|
||||||
///
|
///
|
||||||
@ -444,7 +456,7 @@ class RenderEditable extends RenderBox {
|
|||||||
if (leftArrow && _extentOffset > 2) {
|
if (leftArrow && _extentOffset > 2) {
|
||||||
final TextSelection textSelection = _selectWordAtOffset(TextPosition(offset: _extentOffset - 2));
|
final TextSelection textSelection = _selectWordAtOffset(TextPosition(offset: _extentOffset - 2));
|
||||||
newOffset = textSelection.baseOffset + 1;
|
newOffset = textSelection.baseOffset + 1;
|
||||||
} else if (rightArrow && _extentOffset < text.text.length - 2) {
|
} else if (rightArrow && _extentOffset < text.toPlainText().length - 2) {
|
||||||
final TextSelection textSelection = _selectWordAtOffset(TextPosition(offset: _extentOffset + 1));
|
final TextSelection textSelection = _selectWordAtOffset(TextPosition(offset: _extentOffset + 1));
|
||||||
newOffset = textSelection.extentOffset - 1;
|
newOffset = textSelection.extentOffset - 1;
|
||||||
}
|
}
|
||||||
@ -487,7 +499,7 @@ class RenderEditable extends RenderBox {
|
|||||||
// case that the user wants to unhighlight some text.
|
// case that the user wants to unhighlight some text.
|
||||||
if (position.offset == _extentOffset) {
|
if (position.offset == _extentOffset) {
|
||||||
if (downArrow)
|
if (downArrow)
|
||||||
newOffset = text.text.length;
|
newOffset = text.toPlainText().length;
|
||||||
else if (upArrow)
|
else if (upArrow)
|
||||||
newOffset = 0;
|
newOffset = 0;
|
||||||
_resetCursor = shift;
|
_resetCursor = shift;
|
||||||
@ -554,16 +566,16 @@ class RenderEditable extends RenderBox {
|
|||||||
case _kCKeyCode:
|
case _kCKeyCode:
|
||||||
if (!selection.isCollapsed) {
|
if (!selection.isCollapsed) {
|
||||||
Clipboard.setData(
|
Clipboard.setData(
|
||||||
ClipboardData(text: selection.textInside(text.text)));
|
ClipboardData(text: selection.textInside(text.toPlainText())));
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case _kXKeyCode:
|
case _kXKeyCode:
|
||||||
if (!selection.isCollapsed) {
|
if (!selection.isCollapsed) {
|
||||||
Clipboard.setData(
|
Clipboard.setData(
|
||||||
ClipboardData(text: selection.textInside(text.text)));
|
ClipboardData(text: selection.textInside(text.toPlainText())));
|
||||||
textSelectionDelegate.textEditingValue = TextEditingValue(
|
textSelectionDelegate.textEditingValue = TextEditingValue(
|
||||||
text: selection.textBefore(text.text)
|
text: selection.textBefore(text.toPlainText())
|
||||||
+ selection.textAfter(text.text),
|
+ selection.textAfter(text.toPlainText()),
|
||||||
selection: TextSelection.collapsed(offset: selection.start),
|
selection: TextSelection.collapsed(offset: selection.start),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -601,15 +613,15 @@ class RenderEditable extends RenderBox {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _handleDelete() {
|
void _handleDelete() {
|
||||||
if (selection.textAfter(text.text).isNotEmpty) {
|
if (selection.textAfter(text.toPlainText()).isNotEmpty) {
|
||||||
textSelectionDelegate.textEditingValue = TextEditingValue(
|
textSelectionDelegate.textEditingValue = TextEditingValue(
|
||||||
text: selection.textBefore(text.text)
|
text: selection.textBefore(text.toPlainText())
|
||||||
+ selection.textAfter(text.text).substring(1),
|
+ selection.textAfter(text.toPlainText()).substring(1),
|
||||||
selection: TextSelection.collapsed(offset: selection.start),
|
selection: TextSelection.collapsed(offset: selection.start),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
textSelectionDelegate.textEditingValue = TextEditingValue(
|
textSelectionDelegate.textEditingValue = TextEditingValue(
|
||||||
text: selection.textBefore(text.text),
|
text: selection.textBefore(text.toPlainText()),
|
||||||
selection: TextSelection.collapsed(offset: selection.start),
|
selection: TextSelection.collapsed(offset: selection.start),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -758,6 +770,28 @@ class RenderEditable extends RenderBox {
|
|||||||
markNeedsSemanticsUpdate();
|
markNeedsSemanticsUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether this rendering object will take a full line regardless the text width.
|
||||||
|
bool get forceLine => _forceLine;
|
||||||
|
bool _forceLine = false;
|
||||||
|
set forceLine(bool value) {
|
||||||
|
assert(value != null);
|
||||||
|
if (_forceLine == value)
|
||||||
|
return;
|
||||||
|
_forceLine = value;
|
||||||
|
markNeedsLayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether this rendering object is read only.
|
||||||
|
bool get readOnly => _readOnly;
|
||||||
|
bool _readOnly = false;
|
||||||
|
set readOnly(bool value) {
|
||||||
|
assert(value != null);
|
||||||
|
if (_readOnly == value)
|
||||||
|
return;
|
||||||
|
_readOnly = value;
|
||||||
|
markNeedsSemanticsUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
/// The maximum number of lines for the text to span, wrapping if necessary.
|
/// The maximum number of lines for the text to span, wrapping if necessary.
|
||||||
///
|
///
|
||||||
/// If this is 1 (the default), the text will not wrap, but will extend
|
/// If this is 1 (the default), the text will not wrap, but will extend
|
||||||
@ -983,6 +1017,8 @@ class RenderEditable extends RenderBox {
|
|||||||
return enableInteractiveSelection ?? !obscureText;
|
return enableInteractiveSelection ?? !obscureText;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
double get _caretMargin => _kCaretGap + cursorWidth;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void describeSemanticsConfiguration(SemanticsConfiguration config) {
|
void describeSemanticsConfiguration(SemanticsConfiguration config) {
|
||||||
super.describeSemanticsConfiguration(config);
|
super.describeSemanticsConfiguration(config);
|
||||||
@ -995,7 +1031,8 @@ class RenderEditable extends RenderBox {
|
|||||||
..isMultiline = _isMultiline
|
..isMultiline = _isMultiline
|
||||||
..textDirection = textDirection
|
..textDirection = textDirection
|
||||||
..isFocused = hasFocus
|
..isFocused = hasFocus
|
||||||
..isTextField = true;
|
..isTextField = true
|
||||||
|
..isReadOnly = readOnly;
|
||||||
|
|
||||||
if (hasFocus && selectionEnabled)
|
if (hasFocus && selectionEnabled)
|
||||||
config.onSetSelection = _handleSetSelection;
|
config.onSetSelection = _handleSetSelection;
|
||||||
@ -1526,10 +1563,12 @@ class RenderEditable extends RenderBox {
|
|||||||
assert(constraintWidth != null);
|
assert(constraintWidth != null);
|
||||||
if (_textLayoutLastWidth == constraintWidth)
|
if (_textLayoutLastWidth == constraintWidth)
|
||||||
return;
|
return;
|
||||||
final double caretMargin = _kCaretGap + cursorWidth;
|
final double availableWidth = math.max(0.0, constraintWidth - _caretMargin);
|
||||||
final double availableWidth = math.max(0.0, constraintWidth - caretMargin);
|
|
||||||
final double maxWidth = _isMultiline ? availableWidth : double.infinity;
|
final double maxWidth = _isMultiline ? availableWidth : double.infinity;
|
||||||
_textPainter.layout(minWidth: availableWidth, maxWidth: maxWidth);
|
_textPainter.layout(
|
||||||
|
minWidth: forceLine ? availableWidth : 0,
|
||||||
|
maxWidth: maxWidth,
|
||||||
|
);
|
||||||
_textLayoutLastWidth = constraintWidth;
|
_textLayoutLastWidth = constraintWidth;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1566,8 +1605,10 @@ class RenderEditable extends RenderBox {
|
|||||||
// though we currently don't use those here.
|
// though we currently don't use those here.
|
||||||
// See also RenderParagraph which has a similar issue.
|
// See also RenderParagraph which has a similar issue.
|
||||||
final Size textPainterSize = _textPainter.size;
|
final Size textPainterSize = _textPainter.size;
|
||||||
size = Size(constraints.maxWidth, constraints.constrainHeight(_preferredHeight(constraints.maxWidth)));
|
final double width = forceLine ? constraints.maxWidth : constraints
|
||||||
final Size contentSize = Size(textPainterSize.width + _kCaretGap + cursorWidth, textPainterSize.height);
|
.constrainWidth(_textPainter.size.width + _caretMargin);
|
||||||
|
size = Size(width, constraints.constrainHeight(_preferredHeight(constraints.maxWidth)));
|
||||||
|
final Size contentSize = Size(textPainterSize.width + _caretMargin, textPainterSize.height);
|
||||||
_maxScrollExtent = _getMaxScrollExtent(contentSize);
|
_maxScrollExtent = _getMaxScrollExtent(contentSize);
|
||||||
offset.applyViewportDimension(_viewportExtent);
|
offset.applyViewportDimension(_viewportExtent);
|
||||||
offset.applyContentDimensions(0.0, _maxScrollExtent);
|
offset.applyContentDimensions(0.0, _maxScrollExtent);
|
||||||
|
@ -150,6 +150,29 @@ class TextEditingController extends ValueNotifier<TextEditingValue> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Builds [TextSpan] from current editing value.
|
||||||
|
///
|
||||||
|
/// By default makes text in composing range appear as underlined.
|
||||||
|
/// Descendants can override this method to customize appearance of text.
|
||||||
|
TextSpan buildTextSpan({TextStyle style , bool withComposing}) {
|
||||||
|
if (!value.composing.isValid || !withComposing) {
|
||||||
|
return TextSpan(style: style, text: text);
|
||||||
|
}
|
||||||
|
final TextStyle composingStyle = style.merge(
|
||||||
|
const TextStyle(decoration: TextDecoration.underline),
|
||||||
|
);
|
||||||
|
return TextSpan(
|
||||||
|
style: style,
|
||||||
|
children: <TextSpan>[
|
||||||
|
TextSpan(text: value.composing.textBefore(value.text)),
|
||||||
|
TextSpan(
|
||||||
|
style: composingStyle,
|
||||||
|
text: value.composing.textInside(value.text),
|
||||||
|
),
|
||||||
|
TextSpan(text: value.composing.textAfter(value.text)),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
/// The currently selected [text].
|
/// The currently selected [text].
|
||||||
///
|
///
|
||||||
/// If the selection is collapsed, then this property gives the offset of the
|
/// If the selection is collapsed, then this property gives the offset of the
|
||||||
@ -288,6 +311,8 @@ class EditableText extends StatefulWidget {
|
|||||||
this.maxLines = 1,
|
this.maxLines = 1,
|
||||||
this.minLines,
|
this.minLines,
|
||||||
this.expands = false,
|
this.expands = false,
|
||||||
|
this.forceLine = true,
|
||||||
|
this.textWidthBasis = TextWidthBasis.parent,
|
||||||
this.autofocus = false,
|
this.autofocus = false,
|
||||||
bool showCursor,
|
bool showCursor,
|
||||||
this.showSelectionHandles = false,
|
this.showSelectionHandles = false,
|
||||||
@ -320,6 +345,7 @@ class EditableText extends StatefulWidget {
|
|||||||
assert(autocorrect != null),
|
assert(autocorrect != null),
|
||||||
assert(showSelectionHandles != null),
|
assert(showSelectionHandles != null),
|
||||||
assert(readOnly != null),
|
assert(readOnly != null),
|
||||||
|
assert(forceLine != null),
|
||||||
assert(style != null),
|
assert(style != null),
|
||||||
assert(cursorColor != null),
|
assert(cursorColor != null),
|
||||||
assert(cursorOpacityAnimates != null),
|
assert(cursorOpacityAnimates != null),
|
||||||
@ -368,6 +394,9 @@ class EditableText extends StatefulWidget {
|
|||||||
/// {@endtemplate}
|
/// {@endtemplate}
|
||||||
final bool obscureText;
|
final bool obscureText;
|
||||||
|
|
||||||
|
/// {@macro flutter.widgets.text.DefaultTextStyle.textWidthBasis}
|
||||||
|
final TextWidthBasis textWidthBasis;
|
||||||
|
|
||||||
/// {@template flutter.widgets.editableText.readOnly}
|
/// {@template flutter.widgets.editableText.readOnly}
|
||||||
/// Whether the text can be changed.
|
/// Whether the text can be changed.
|
||||||
///
|
///
|
||||||
@ -378,6 +407,18 @@ class EditableText extends StatefulWidget {
|
|||||||
/// {@endtemplate}
|
/// {@endtemplate}
|
||||||
final bool readOnly;
|
final bool readOnly;
|
||||||
|
|
||||||
|
/// Whether the text will take the full width regardless of the text width.
|
||||||
|
///
|
||||||
|
/// When this is set to false, the width will be based on text width, which
|
||||||
|
/// will also be affected by [textWidthBasis].
|
||||||
|
///
|
||||||
|
/// Defaults to true. Must not be null.
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [textWidthBasis], which controls the calculation of text width.
|
||||||
|
final bool forceLine;
|
||||||
|
|
||||||
/// Whether to show selection handles.
|
/// Whether to show selection handles.
|
||||||
///
|
///
|
||||||
/// When a selection is active, there will be two handles at each side of
|
/// When a selection is active, there will be two handles at each side of
|
||||||
@ -396,7 +437,7 @@ class EditableText extends StatefulWidget {
|
|||||||
///
|
///
|
||||||
/// See also:
|
/// See also:
|
||||||
///
|
///
|
||||||
/// * [showSelectionHandles], which controls the visibility of the selection handles..
|
/// * [showSelectionHandles], which controls the visibility of the selection handles.
|
||||||
/// {@endtemplate}
|
/// {@endtemplate}
|
||||||
final bool showCursor;
|
final bool showCursor;
|
||||||
|
|
||||||
@ -1622,6 +1663,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||||||
showCursor: EditableText.debugDeterministicCursor
|
showCursor: EditableText.debugDeterministicCursor
|
||||||
? ValueNotifier<bool>(widget.showCursor)
|
? ValueNotifier<bool>(widget.showCursor)
|
||||||
: _cursorVisibilityNotifier,
|
: _cursorVisibilityNotifier,
|
||||||
|
forceLine: widget.forceLine,
|
||||||
|
readOnly: widget.readOnly,
|
||||||
hasFocus: _hasFocus,
|
hasFocus: _hasFocus,
|
||||||
maxLines: widget.maxLines,
|
maxLines: widget.maxLines,
|
||||||
minLines: widget.minLines,
|
minLines: widget.minLines,
|
||||||
@ -1632,6 +1675,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||||||
textAlign: widget.textAlign,
|
textAlign: widget.textAlign,
|
||||||
textDirection: _textDirection,
|
textDirection: _textDirection,
|
||||||
locale: widget.locale,
|
locale: widget.locale,
|
||||||
|
textWidthBasis: widget.textWidthBasis,
|
||||||
obscureText: widget.obscureText,
|
obscureText: widget.obscureText,
|
||||||
autocorrect: widget.autocorrect,
|
autocorrect: widget.autocorrect,
|
||||||
offset: offset,
|
offset: offset,
|
||||||
@ -1657,32 +1701,20 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||||||
/// By default makes text in composing range appear as underlined.
|
/// By default makes text in composing range appear as underlined.
|
||||||
/// Descendants can override this method to customize appearance of text.
|
/// Descendants can override this method to customize appearance of text.
|
||||||
TextSpan buildTextSpan() {
|
TextSpan buildTextSpan() {
|
||||||
// Read only mode should not paint text composing.
|
|
||||||
if (!widget.obscureText && _value.composing.isValid && !widget.readOnly) {
|
|
||||||
final TextStyle composingStyle = widget.style.merge(
|
|
||||||
const TextStyle(decoration: TextDecoration.underline),
|
|
||||||
);
|
|
||||||
return TextSpan(
|
|
||||||
style: widget.style,
|
|
||||||
children: <TextSpan>[
|
|
||||||
TextSpan(text: _value.composing.textBefore(_value.text)),
|
|
||||||
TextSpan(
|
|
||||||
style: composingStyle,
|
|
||||||
text: _value.composing.textInside(_value.text),
|
|
||||||
),
|
|
||||||
TextSpan(text: _value.composing.textAfter(_value.text)),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
String text = _value.text;
|
|
||||||
if (widget.obscureText) {
|
if (widget.obscureText) {
|
||||||
|
String text = _value.text;
|
||||||
text = RenderEditable.obscuringCharacter * text.length;
|
text = RenderEditable.obscuringCharacter * text.length;
|
||||||
final int o =
|
final int o =
|
||||||
_obscureShowCharTicksPending > 0 ? _obscureLatestCharIndex : null;
|
_obscureShowCharTicksPending > 0 ? _obscureLatestCharIndex : null;
|
||||||
if (o != null && o >= 0 && o < text.length)
|
if (o != null && o >= 0 && o < text.length)
|
||||||
text = text.replaceRange(o, o + 1, _value.text.substring(o, o + 1));
|
text = text.replaceRange(o, o + 1, _value.text.substring(o, o + 1));
|
||||||
|
return TextSpan(style: widget.style, text: text);
|
||||||
}
|
}
|
||||||
return TextSpan(style: widget.style, text: text);
|
// Read only mode should not paint text composing.
|
||||||
|
return widget.controller.buildTextSpan(
|
||||||
|
style: widget.style,
|
||||||
|
withComposing: !widget.readOnly,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1696,6 +1728,9 @@ class _Editable extends LeafRenderObjectWidget {
|
|||||||
this.cursorColor,
|
this.cursorColor,
|
||||||
this.backgroundCursorColor,
|
this.backgroundCursorColor,
|
||||||
this.showCursor,
|
this.showCursor,
|
||||||
|
this.forceLine,
|
||||||
|
this.readOnly,
|
||||||
|
this.textWidthBasis,
|
||||||
this.hasFocus,
|
this.hasFocus,
|
||||||
this.maxLines,
|
this.maxLines,
|
||||||
this.minLines,
|
this.minLines,
|
||||||
@ -1730,6 +1765,8 @@ class _Editable extends LeafRenderObjectWidget {
|
|||||||
final LayerLink endHandleLayerLink;
|
final LayerLink endHandleLayerLink;
|
||||||
final Color backgroundCursorColor;
|
final Color backgroundCursorColor;
|
||||||
final ValueNotifier<bool> showCursor;
|
final ValueNotifier<bool> showCursor;
|
||||||
|
final bool forceLine;
|
||||||
|
final bool readOnly;
|
||||||
final bool hasFocus;
|
final bool hasFocus;
|
||||||
final int maxLines;
|
final int maxLines;
|
||||||
final int minLines;
|
final int minLines;
|
||||||
@ -1741,6 +1778,7 @@ class _Editable extends LeafRenderObjectWidget {
|
|||||||
final TextDirection textDirection;
|
final TextDirection textDirection;
|
||||||
final Locale locale;
|
final Locale locale;
|
||||||
final bool obscureText;
|
final bool obscureText;
|
||||||
|
final TextWidthBasis textWidthBasis;
|
||||||
final bool autocorrect;
|
final bool autocorrect;
|
||||||
final ViewportOffset offset;
|
final ViewportOffset offset;
|
||||||
final SelectionChangedHandler onSelectionChanged;
|
final SelectionChangedHandler onSelectionChanged;
|
||||||
@ -1763,6 +1801,8 @@ class _Editable extends LeafRenderObjectWidget {
|
|||||||
endHandleLayerLink: endHandleLayerLink,
|
endHandleLayerLink: endHandleLayerLink,
|
||||||
backgroundCursorColor: backgroundCursorColor,
|
backgroundCursorColor: backgroundCursorColor,
|
||||||
showCursor: showCursor,
|
showCursor: showCursor,
|
||||||
|
forceLine: forceLine,
|
||||||
|
readOnly: readOnly,
|
||||||
hasFocus: hasFocus,
|
hasFocus: hasFocus,
|
||||||
maxLines: maxLines,
|
maxLines: maxLines,
|
||||||
minLines: minLines,
|
minLines: minLines,
|
||||||
@ -1779,6 +1819,7 @@ class _Editable extends LeafRenderObjectWidget {
|
|||||||
onCaretChanged: onCaretChanged,
|
onCaretChanged: onCaretChanged,
|
||||||
ignorePointer: rendererIgnoresPointer,
|
ignorePointer: rendererIgnoresPointer,
|
||||||
obscureText: obscureText,
|
obscureText: obscureText,
|
||||||
|
textWidthBasis: textWidthBasis,
|
||||||
cursorWidth: cursorWidth,
|
cursorWidth: cursorWidth,
|
||||||
cursorRadius: cursorRadius,
|
cursorRadius: cursorRadius,
|
||||||
cursorOffset: cursorOffset,
|
cursorOffset: cursorOffset,
|
||||||
@ -1797,6 +1838,8 @@ class _Editable extends LeafRenderObjectWidget {
|
|||||||
..startHandleLayerLink = startHandleLayerLink
|
..startHandleLayerLink = startHandleLayerLink
|
||||||
..endHandleLayerLink = endHandleLayerLink
|
..endHandleLayerLink = endHandleLayerLink
|
||||||
..showCursor = showCursor
|
..showCursor = showCursor
|
||||||
|
..forceLine = forceLine
|
||||||
|
..readOnly = readOnly
|
||||||
..hasFocus = hasFocus
|
..hasFocus = hasFocus
|
||||||
..maxLines = maxLines
|
..maxLines = maxLines
|
||||||
..minLines = minLines
|
..minLines = minLines
|
||||||
@ -1812,6 +1855,7 @@ class _Editable extends LeafRenderObjectWidget {
|
|||||||
..onSelectionChanged = onSelectionChanged
|
..onSelectionChanged = onSelectionChanged
|
||||||
..onCaretChanged = onCaretChanged
|
..onCaretChanged = onCaretChanged
|
||||||
..ignorePointer = rendererIgnoresPointer
|
..ignorePointer = rendererIgnoresPointer
|
||||||
|
..textWidthBasis = textWidthBasis
|
||||||
..obscureText = obscureText
|
..obscureText = obscureText
|
||||||
..cursorWidth = cursorWidth
|
..cursorWidth = cursorWidth
|
||||||
..cursorRadius = cursorRadius
|
..cursorRadius = cursorRadius
|
||||||
|
@ -973,7 +973,7 @@ void main() {
|
|||||||
|
|
||||||
final RenderEditable renderEditable = findRenderEditable(tester);
|
final RenderEditable renderEditable = findRenderEditable(tester);
|
||||||
// There should be no composing.
|
// There should be no composing.
|
||||||
expect(renderEditable.isComposingText, false);
|
expect(renderEditable.text, TextSpan(text:'readonly', style: renderEditable.text.style));
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('Dynamically switching between read only and not read only should hide or show collapse cursor', (WidgetTester tester) async {
|
testWidgets('Dynamically switching between read only and not read only should hide or show collapse cursor', (WidgetTester tester) async {
|
||||||
@ -3231,6 +3231,30 @@ void main() {
|
|||||||
semantics.dispose();
|
semantics.dispose();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('Read only TextField identifies as read only text field in semantics', (WidgetTester tester) async {
|
||||||
|
final SemanticsTester semantics = SemanticsTester(tester);
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
const MaterialApp(
|
||||||
|
home: Material(
|
||||||
|
child: Center(
|
||||||
|
child: TextField(
|
||||||
|
maxLength: 10,
|
||||||
|
readOnly: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
semantics,
|
||||||
|
includesNodeWith(flags: <SemanticsFlag>[SemanticsFlag.isTextField, SemanticsFlag.isReadOnly])
|
||||||
|
);
|
||||||
|
|
||||||
|
semantics.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
void sendFakeKeyEvent(Map<String, dynamic> data) {
|
void sendFakeKeyEvent(Map<String, dynamic> data) {
|
||||||
defaultBinaryMessenger.handlePlatformMessage(
|
defaultBinaryMessenger.handlePlatformMessage(
|
||||||
SystemChannels.keyEvent.name,
|
SystemChannels.keyEvent.name,
|
||||||
|
3722
packages/flutter/test/widgets/selectable_text_test.dart
Normal file
3722
packages/flutter/test/widgets/selectable_text_test.dart
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user