Stop using SelectionChangedCause internally to show the text selection toolbar (#27534)
This commit is contained in:
parent
c920cb7c57
commit
ee30499a7e
@ -456,15 +456,18 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _handleForcePressStarted(ForcePressDetails details) {
|
void _handleForcePressStarted(ForcePressDetails details) {
|
||||||
// The cause is not keyboard press but we would still like to just
|
_renderEditable.selectWordsInRange(
|
||||||
// highlight the word without showing any handles or toolbar.
|
from: details.globalPosition,
|
||||||
_renderEditable.selectWordsInRange(from: details.globalPosition, cause: SelectionChangedCause.keyboard);
|
cause: SelectionChangedCause.forcePress,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleForcePressEnded(ForcePressDetails details) {
|
void _handleForcePressEnded(ForcePressDetails details) {
|
||||||
// The cause is not technically double tap, but we would still like to show
|
_renderEditable.selectWordsInRange(
|
||||||
// the toolbar and handles.
|
from: details.globalPosition,
|
||||||
_renderEditable.selectWordsInRange(from: details.globalPosition, cause: SelectionChangedCause.doubleTap);
|
cause: SelectionChangedCause.forcePress,
|
||||||
|
);
|
||||||
|
_editableTextKey.currentState.showToolbar();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleSingleTapUp(TapUpDetails details) {
|
void _handleSingleTapUp(TapUpDetails details) {
|
||||||
@ -474,10 +477,12 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK
|
|||||||
|
|
||||||
void _handleSingleLongTapDown() {
|
void _handleSingleLongTapDown() {
|
||||||
_renderEditable.selectPosition(cause: SelectionChangedCause.longPress);
|
_renderEditable.selectPosition(cause: SelectionChangedCause.longPress);
|
||||||
|
_editableTextKey.currentState.showToolbar();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleDoubleTapDown(TapDownDetails details) {
|
void _handleDoubleTapDown(TapDownDetails details) {
|
||||||
_renderEditable.selectWord(cause: SelectionChangedCause.doubleTap);
|
_renderEditable.selectWord(cause: SelectionChangedCause.tap);
|
||||||
|
_editableTextKey.currentState.showToolbar();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -582,11 +582,6 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
|
|||||||
_editableTextKey.currentState?.requestKeyboard();
|
_editableTextKey.currentState?.requestKeyboard();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleSelectionChanged(TextSelection selection, SelectionChangedCause cause) {
|
|
||||||
if (cause == SelectionChangedCause.longPress)
|
|
||||||
Feedback.forLongPress(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
InteractiveInkFeature _createInkFeature(TapDownDetails details) {
|
InteractiveInkFeature _createInkFeature(TapDownDetails details) {
|
||||||
final MaterialInkController inkController = Material.of(context);
|
final MaterialInkController inkController = Material.of(context);
|
||||||
final ThemeData themeData = Theme.of(context);
|
final ThemeData themeData = Theme.of(context);
|
||||||
@ -630,9 +625,11 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
|
|||||||
|
|
||||||
void _handleForcePressStarted(ForcePressDetails details) {
|
void _handleForcePressStarted(ForcePressDetails details) {
|
||||||
if (widget.selectionEnabled) {
|
if (widget.selectionEnabled) {
|
||||||
// The cause is not technically double tap, but we would like to show
|
_renderEditable.selectWordsInRange(
|
||||||
// the toolbar.
|
from: details.globalPosition,
|
||||||
_renderEditable.selectWordsInRange(from: details.globalPosition, cause: SelectionChangedCause.doubleTap);
|
cause: SelectionChangedCause.forcePress,
|
||||||
|
);
|
||||||
|
_editableTextKey.currentState.showToolbar();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -667,14 +664,19 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
|
|||||||
case TargetPlatform.android:
|
case TargetPlatform.android:
|
||||||
case TargetPlatform.fuchsia:
|
case TargetPlatform.fuchsia:
|
||||||
_renderEditable.selectWord(cause: SelectionChangedCause.longPress);
|
_renderEditable.selectWord(cause: SelectionChangedCause.longPress);
|
||||||
|
Feedback.forLongPress(context);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
_editableTextKey.currentState.showToolbar();
|
||||||
}
|
}
|
||||||
_confirmCurrentSplash();
|
_confirmCurrentSplash();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleDoubleTapDown(TapDownDetails details) {
|
void _handleDoubleTapDown(TapDownDetails details) {
|
||||||
_renderEditable.selectWord(cause: SelectionChangedCause.doubleTap);
|
if (widget.selectionEnabled) {
|
||||||
|
_renderEditable.selectWord(cause: SelectionChangedCause.doubleTap);
|
||||||
|
_editableTextKey.currentState.showToolbar();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _startSplash(TapDownDetails details) {
|
void _startSplash(TapDownDetails details) {
|
||||||
@ -784,7 +786,6 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
|
|||||||
onChanged: widget.onChanged,
|
onChanged: widget.onChanged,
|
||||||
onEditingComplete: widget.onEditingComplete,
|
onEditingComplete: widget.onEditingComplete,
|
||||||
onSubmitted: widget.onSubmitted,
|
onSubmitted: widget.onSubmitted,
|
||||||
onSelectionChanged: _handleSelectionChanged,
|
|
||||||
inputFormatters: formatters,
|
inputFormatters: formatters,
|
||||||
rendererIgnoresPointer: true,
|
rendererIgnoresPointer: true,
|
||||||
cursorWidth: widget.cursorWidth,
|
cursorWidth: widget.cursorWidth,
|
||||||
|
@ -45,6 +45,10 @@ enum SelectionChangedCause {
|
|||||||
/// location of the cursor) to change.
|
/// location of the cursor) to change.
|
||||||
longPress,
|
longPress,
|
||||||
|
|
||||||
|
/// The user force-pressed the text and that caused the selection (or the
|
||||||
|
/// location of the cursor) to change.
|
||||||
|
forcePress,
|
||||||
|
|
||||||
/// The user used the keyboard to change the selection or the location of the
|
/// The user used the keyboard to change the selection or the location of the
|
||||||
/// cursor.
|
/// cursor.
|
||||||
///
|
///
|
||||||
@ -737,12 +741,12 @@ class RenderEditable extends RenderBox {
|
|||||||
markNeedsLayout();
|
markNeedsLayout();
|
||||||
}
|
}
|
||||||
|
|
||||||
///{@template flutter.rendering.editable.paintCursorOnTop}
|
/// {@template flutter.rendering.editable.paintCursorOnTop}
|
||||||
/// If the cursor should be painted on top of the text or underneath it.
|
/// If the cursor should be painted on top of the text or underneath it.
|
||||||
///
|
///
|
||||||
/// By default, the cursor should be painted on top for iOS platforms and
|
/// By default, the cursor should be painted on top for iOS platforms and
|
||||||
/// underneath for Android platforms.
|
/// underneath for Android platforms.
|
||||||
/// {@end template}
|
/// {@endtemplate}
|
||||||
bool get paintCursorAboveText => _paintCursorOnTop;
|
bool get paintCursorAboveText => _paintCursorOnTop;
|
||||||
bool _paintCursorOnTop;
|
bool _paintCursorOnTop;
|
||||||
set paintCursorAboveText(bool value) {
|
set paintCursorAboveText(bool value) {
|
||||||
@ -759,7 +763,7 @@ class RenderEditable extends RenderBox {
|
|||||||
/// (-[cursorWidth] * 0.5, 0.0) on iOS platforms and (0, 0) on Android
|
/// (-[cursorWidth] * 0.5, 0.0) on iOS platforms and (0, 0) on Android
|
||||||
/// platforms. The origin from where the offset is applied to is the arbitrary
|
/// platforms. The origin from where the offset is applied to is the arbitrary
|
||||||
/// location where the cursor ends up being rendered from by default.
|
/// location where the cursor ends up being rendered from by default.
|
||||||
/// {@end template}
|
/// {@endtemplate}
|
||||||
Offset get cursorOffset => _cursorOffset;
|
Offset get cursorOffset => _cursorOffset;
|
||||||
Offset _cursorOffset;
|
Offset _cursorOffset;
|
||||||
set cursorOffset(Offset value) {
|
set cursorOffset(Offset value) {
|
||||||
@ -1233,6 +1237,15 @@ class RenderEditable extends RenderBox {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Move selection to the location of the last tap down.
|
/// Move selection to the location of the last tap down.
|
||||||
|
///
|
||||||
|
/// {@template flutter.rendering.editable.select}
|
||||||
|
/// This method is mainly used to translate user inputs in global positions
|
||||||
|
/// into a [TextSelection]. When used in conjunction with a [EditableText],
|
||||||
|
/// the selection change is fed back into [TextEditingController.selection].
|
||||||
|
///
|
||||||
|
/// If you have a [TextEditingController], it's generally easier to
|
||||||
|
/// programmatically manipulate its `value` or `selection` directly.
|
||||||
|
/// {@endtemplate}
|
||||||
void selectPosition({@required SelectionChangedCause cause}) {
|
void selectPosition({@required SelectionChangedCause cause}) {
|
||||||
assert(cause != null);
|
assert(cause != null);
|
||||||
_layoutText(constraints.maxWidth);
|
_layoutText(constraints.maxWidth);
|
||||||
@ -1244,6 +1257,8 @@ class RenderEditable extends RenderBox {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Select a word around the location of the last tap down.
|
/// Select a word around the location of the last tap down.
|
||||||
|
///
|
||||||
|
/// {@macro flutter.rendering.editable.select}
|
||||||
void selectWord({@required SelectionChangedCause cause}) {
|
void selectWord({@required SelectionChangedCause cause}) {
|
||||||
selectWordsInRange(from: _lastTapDownPosition, cause: cause);
|
selectWordsInRange(from: _lastTapDownPosition, cause: cause);
|
||||||
}
|
}
|
||||||
@ -1252,6 +1267,8 @@ class RenderEditable extends RenderBox {
|
|||||||
///
|
///
|
||||||
/// The first and last endpoints of the selection will always be at the
|
/// The first and last endpoints of the selection will always be at the
|
||||||
/// beginning and end of a word respectively.
|
/// beginning and end of a word respectively.
|
||||||
|
///
|
||||||
|
/// {@macro flutter.rendering.editable.select}
|
||||||
void selectWordsInRange({@required Offset from, Offset to, @required SelectionChangedCause cause}) {
|
void selectWordsInRange({@required Offset from, Offset to, @required SelectionChangedCause cause}) {
|
||||||
assert(cause != null);
|
assert(cause != null);
|
||||||
_layoutText(constraints.maxWidth);
|
_layoutText(constraints.maxWidth);
|
||||||
@ -1272,6 +1289,8 @@ class RenderEditable extends RenderBox {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Move the selection to the beginning or end of a word.
|
/// Move the selection to the beginning or end of a word.
|
||||||
|
///
|
||||||
|
/// {@macro flutter.rendering.editable.select}
|
||||||
void selectWordEdge({@required SelectionChangedCause cause}) {
|
void selectWordEdge({@required SelectionChangedCause cause}) {
|
||||||
assert(cause != null);
|
assert(cause != null);
|
||||||
_layoutText(constraints.maxWidth);
|
_layoutText(constraints.maxWidth);
|
||||||
|
@ -745,7 +745,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||||||
renderEditable.setFloatingCursor(FloatingCursorDragState.End, finalPosition, _lastTextPosition);
|
renderEditable.setFloatingCursor(FloatingCursorDragState.End, finalPosition, _lastTextPosition);
|
||||||
if (_lastTextPosition.offset != renderEditable.selection.baseOffset)
|
if (_lastTextPosition.offset != renderEditable.selection.baseOffset)
|
||||||
// The cause is technically the force cursor, but the cause is listed as tap as the desired functionality is the same.
|
// The cause is technically the force cursor, but the cause is listed as tap as the desired functionality is the same.
|
||||||
_handleSelectionChanged(TextSelection.collapsed(offset: _lastTextPosition.offset), renderEditable, SelectionChangedCause.tap);
|
_handleSelectionChanged(TextSelection.collapsed(offset: _lastTextPosition.offset), renderEditable, SelectionChangedCause.forcePress);
|
||||||
_startCaretRect = null;
|
_startCaretRect = null;
|
||||||
_lastTextPosition = null;
|
_lastTextPosition = null;
|
||||||
_pointOffsetOrigin = null;
|
_pointOffsetOrigin = null;
|
||||||
@ -923,8 +923,6 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||||||
final bool longPress = cause == SelectionChangedCause.longPress;
|
final bool longPress = cause == SelectionChangedCause.longPress;
|
||||||
if (cause != SelectionChangedCause.keyboard && (_value.text.isNotEmpty || longPress))
|
if (cause != SelectionChangedCause.keyboard && (_value.text.isNotEmpty || longPress))
|
||||||
_selectionOverlay.showHandles();
|
_selectionOverlay.showHandles();
|
||||||
if (longPress || cause == SelectionChangedCause.doubleTap)
|
|
||||||
_selectionOverlay.showToolbar();
|
|
||||||
if (widget.onSelectionChanged != null)
|
if (widget.onSelectionChanged != null)
|
||||||
widget.onSelectionChanged(selection, cause);
|
widget.onSelectionChanged(selection, cause);
|
||||||
}
|
}
|
||||||
@ -1150,6 +1148,18 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||||||
_scrollController.jumpTo(_getScrollOffsetForCaret(renderEditable.getLocalRectForCaret(position)));
|
_scrollController.jumpTo(_getScrollOffsetForCaret(renderEditable.getLocalRectForCaret(position)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Shows the selection toolbar at the location of the current cursor.
|
||||||
|
///
|
||||||
|
/// Returns `false` if a toolbar couldn't be shown such as when no text
|
||||||
|
/// selection currently exists.
|
||||||
|
bool showToolbar() {
|
||||||
|
if (_selectionOverlay == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
_selectionOverlay.showToolbar();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void hideToolbar() {
|
void hideToolbar() {
|
||||||
_selectionOverlay?.hide();
|
_selectionOverlay?.hide();
|
||||||
|
@ -71,6 +71,7 @@ void main() {
|
|||||||
// Long-press to bring up the text editing controls.
|
// Long-press to bring up the text editing controls.
|
||||||
final Finder textFinder = find.byKey(editableTextKey);
|
final Finder textFinder = find.byKey(editableTextKey);
|
||||||
await tester.longPress(textFinder);
|
await tester.longPress(textFinder);
|
||||||
|
tester.state<EditableTextState>(textFinder).showToolbar();
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
await tester.pump(const Duration(milliseconds: 500));
|
await tester.pump(const Duration(milliseconds: 500));
|
||||||
await tester.pump(const Duration(milliseconds: 500));
|
await tester.pump(const Duration(milliseconds: 500));
|
||||||
@ -125,6 +126,7 @@ void main() {
|
|||||||
// Long-press to bring up the text editing controls.
|
// Long-press to bring up the text editing controls.
|
||||||
final Finder textFinder = find.byKey(editableTextKey);
|
final Finder textFinder = find.byKey(editableTextKey);
|
||||||
await tester.longPress(textFinder);
|
await tester.longPress(textFinder);
|
||||||
|
tester.state<EditableTextState>(textFinder).showToolbar();
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
|
|
||||||
await tester.tap(find.text('PASTE'));
|
await tester.tap(find.text('PASTE'));
|
||||||
|
@ -478,6 +478,45 @@ void main() {
|
|||||||
equals('TextInputAction.done'));
|
equals('TextInputAction.done'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('can only show toolbar when there is text and a selection',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: EditableText(
|
||||||
|
backgroundCursorColor: Colors.grey,
|
||||||
|
controller: controller,
|
||||||
|
focusNode: focusNode,
|
||||||
|
style: textStyle,
|
||||||
|
cursorColor: cursorColor,
|
||||||
|
selectionControls: materialTextSelectionControls,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final EditableTextState state =
|
||||||
|
tester.state<EditableTextState>(find.byType(EditableText));
|
||||||
|
|
||||||
|
expect(state.showToolbar(), false);
|
||||||
|
await tester.pump();
|
||||||
|
expect(find.text('PASTE'), findsNothing);
|
||||||
|
|
||||||
|
controller.text = 'blah';
|
||||||
|
await tester.pump();
|
||||||
|
expect(state.showToolbar(), false);
|
||||||
|
await tester.pump();
|
||||||
|
expect(find.text('PASTE'), findsNothing);
|
||||||
|
|
||||||
|
// Select something. Doesn't really matter what.
|
||||||
|
state.renderEditable.selectWordsInRange(
|
||||||
|
from: const Offset(0, 0),
|
||||||
|
cause: SelectionChangedCause.tap,
|
||||||
|
);
|
||||||
|
await tester.pump();
|
||||||
|
expect(state.showToolbar(), true);
|
||||||
|
await tester.pump();
|
||||||
|
expect(find.text('PASTE'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('Fires onChanged when text changes via TextSelectionOverlay',
|
testWidgets('Fires onChanged when text changes via TextSelectionOverlay',
|
||||||
(WidgetTester tester) async {
|
(WidgetTester tester) async {
|
||||||
final GlobalKey<EditableTextState> editableTextKey =
|
final GlobalKey<EditableTextState> editableTextKey =
|
||||||
@ -513,6 +552,7 @@ void main() {
|
|||||||
// Long-press to bring up the text editing controls.
|
// Long-press to bring up the text editing controls.
|
||||||
final Finder textFinder = find.byKey(editableTextKey);
|
final Finder textFinder = find.byKey(editableTextKey);
|
||||||
await tester.longPress(textFinder);
|
await tester.longPress(textFinder);
|
||||||
|
tester.state<EditableTextState>(textFinder).showToolbar();
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
|
|
||||||
await tester.tap(find.text('PASTE'));
|
await tester.tap(find.text('PASTE'));
|
||||||
|
Loading…
x
Reference in New Issue
Block a user