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) {
|
||||
// The cause is not keyboard press but we would still like to just
|
||||
// highlight the word without showing any handles or toolbar.
|
||||
_renderEditable.selectWordsInRange(from: details.globalPosition, cause: SelectionChangedCause.keyboard);
|
||||
_renderEditable.selectWordsInRange(
|
||||
from: details.globalPosition,
|
||||
cause: SelectionChangedCause.forcePress,
|
||||
);
|
||||
}
|
||||
|
||||
void _handleForcePressEnded(ForcePressDetails details) {
|
||||
// The cause is not technically double tap, but we would still like to show
|
||||
// the toolbar and handles.
|
||||
_renderEditable.selectWordsInRange(from: details.globalPosition, cause: SelectionChangedCause.doubleTap);
|
||||
_renderEditable.selectWordsInRange(
|
||||
from: details.globalPosition,
|
||||
cause: SelectionChangedCause.forcePress,
|
||||
);
|
||||
_editableTextKey.currentState.showToolbar();
|
||||
}
|
||||
|
||||
void _handleSingleTapUp(TapUpDetails details) {
|
||||
@ -474,10 +477,12 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK
|
||||
|
||||
void _handleSingleLongTapDown() {
|
||||
_renderEditable.selectPosition(cause: SelectionChangedCause.longPress);
|
||||
_editableTextKey.currentState.showToolbar();
|
||||
}
|
||||
|
||||
void _handleDoubleTapDown(TapDownDetails details) {
|
||||
_renderEditable.selectWord(cause: SelectionChangedCause.doubleTap);
|
||||
_renderEditable.selectWord(cause: SelectionChangedCause.tap);
|
||||
_editableTextKey.currentState.showToolbar();
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -582,11 +582,6 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
|
||||
_editableTextKey.currentState?.requestKeyboard();
|
||||
}
|
||||
|
||||
void _handleSelectionChanged(TextSelection selection, SelectionChangedCause cause) {
|
||||
if (cause == SelectionChangedCause.longPress)
|
||||
Feedback.forLongPress(context);
|
||||
}
|
||||
|
||||
InteractiveInkFeature _createInkFeature(TapDownDetails details) {
|
||||
final MaterialInkController inkController = Material.of(context);
|
||||
final ThemeData themeData = Theme.of(context);
|
||||
@ -630,9 +625,11 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
|
||||
|
||||
void _handleForcePressStarted(ForcePressDetails details) {
|
||||
if (widget.selectionEnabled) {
|
||||
// The cause is not technically double tap, but we would like to show
|
||||
// the toolbar.
|
||||
_renderEditable.selectWordsInRange(from: details.globalPosition, cause: SelectionChangedCause.doubleTap);
|
||||
_renderEditable.selectWordsInRange(
|
||||
from: details.globalPosition,
|
||||
cause: SelectionChangedCause.forcePress,
|
||||
);
|
||||
_editableTextKey.currentState.showToolbar();
|
||||
}
|
||||
}
|
||||
|
||||
@ -667,14 +664,19 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
|
||||
case TargetPlatform.android:
|
||||
case TargetPlatform.fuchsia:
|
||||
_renderEditable.selectWord(cause: SelectionChangedCause.longPress);
|
||||
Feedback.forLongPress(context);
|
||||
break;
|
||||
}
|
||||
_editableTextKey.currentState.showToolbar();
|
||||
}
|
||||
_confirmCurrentSplash();
|
||||
}
|
||||
|
||||
void _handleDoubleTapDown(TapDownDetails details) {
|
||||
_renderEditable.selectWord(cause: SelectionChangedCause.doubleTap);
|
||||
if (widget.selectionEnabled) {
|
||||
_renderEditable.selectWord(cause: SelectionChangedCause.doubleTap);
|
||||
_editableTextKey.currentState.showToolbar();
|
||||
}
|
||||
}
|
||||
|
||||
void _startSplash(TapDownDetails details) {
|
||||
@ -784,7 +786,6 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
|
||||
onChanged: widget.onChanged,
|
||||
onEditingComplete: widget.onEditingComplete,
|
||||
onSubmitted: widget.onSubmitted,
|
||||
onSelectionChanged: _handleSelectionChanged,
|
||||
inputFormatters: formatters,
|
||||
rendererIgnoresPointer: true,
|
||||
cursorWidth: widget.cursorWidth,
|
||||
|
@ -45,6 +45,10 @@ enum SelectionChangedCause {
|
||||
/// location of the cursor) to change.
|
||||
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
|
||||
/// cursor.
|
||||
///
|
||||
@ -737,12 +741,12 @@ class RenderEditable extends RenderBox {
|
||||
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.
|
||||
///
|
||||
/// By default, the cursor should be painted on top for iOS platforms and
|
||||
/// underneath for Android platforms.
|
||||
/// {@end template}
|
||||
/// {@endtemplate}
|
||||
bool get paintCursorAboveText => _paintCursorOnTop;
|
||||
bool _paintCursorOnTop;
|
||||
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
|
||||
/// platforms. The origin from where the offset is applied to is the arbitrary
|
||||
/// location where the cursor ends up being rendered from by default.
|
||||
/// {@end template}
|
||||
/// {@endtemplate}
|
||||
Offset get cursorOffset => _cursorOffset;
|
||||
Offset _cursorOffset;
|
||||
set cursorOffset(Offset value) {
|
||||
@ -1233,6 +1237,15 @@ class RenderEditable extends RenderBox {
|
||||
}
|
||||
|
||||
/// 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}) {
|
||||
assert(cause != null);
|
||||
_layoutText(constraints.maxWidth);
|
||||
@ -1244,6 +1257,8 @@ class RenderEditable extends RenderBox {
|
||||
}
|
||||
|
||||
/// Select a word around the location of the last tap down.
|
||||
///
|
||||
/// {@macro flutter.rendering.editable.select}
|
||||
void selectWord({@required SelectionChangedCause 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
|
||||
/// beginning and end of a word respectively.
|
||||
///
|
||||
/// {@macro flutter.rendering.editable.select}
|
||||
void selectWordsInRange({@required Offset from, Offset to, @required SelectionChangedCause cause}) {
|
||||
assert(cause != null);
|
||||
_layoutText(constraints.maxWidth);
|
||||
@ -1272,6 +1289,8 @@ class RenderEditable extends RenderBox {
|
||||
}
|
||||
|
||||
/// Move the selection to the beginning or end of a word.
|
||||
///
|
||||
/// {@macro flutter.rendering.editable.select}
|
||||
void selectWordEdge({@required SelectionChangedCause cause}) {
|
||||
assert(cause != null);
|
||||
_layoutText(constraints.maxWidth);
|
||||
|
@ -745,7 +745,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
||||
renderEditable.setFloatingCursor(FloatingCursorDragState.End, finalPosition, _lastTextPosition);
|
||||
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.
|
||||
_handleSelectionChanged(TextSelection.collapsed(offset: _lastTextPosition.offset), renderEditable, SelectionChangedCause.tap);
|
||||
_handleSelectionChanged(TextSelection.collapsed(offset: _lastTextPosition.offset), renderEditable, SelectionChangedCause.forcePress);
|
||||
_startCaretRect = null;
|
||||
_lastTextPosition = null;
|
||||
_pointOffsetOrigin = null;
|
||||
@ -923,8 +923,6 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
||||
final bool longPress = cause == SelectionChangedCause.longPress;
|
||||
if (cause != SelectionChangedCause.keyboard && (_value.text.isNotEmpty || longPress))
|
||||
_selectionOverlay.showHandles();
|
||||
if (longPress || cause == SelectionChangedCause.doubleTap)
|
||||
_selectionOverlay.showToolbar();
|
||||
if (widget.onSelectionChanged != null)
|
||||
widget.onSelectionChanged(selection, cause);
|
||||
}
|
||||
@ -1150,6 +1148,18 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
||||
_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
|
||||
void hideToolbar() {
|
||||
_selectionOverlay?.hide();
|
||||
|
@ -71,6 +71,7 @@ void main() {
|
||||
// Long-press to bring up the text editing controls.
|
||||
final Finder textFinder = find.byKey(editableTextKey);
|
||||
await tester.longPress(textFinder);
|
||||
tester.state<EditableTextState>(textFinder).showToolbar();
|
||||
await tester.pump();
|
||||
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.
|
||||
final Finder textFinder = find.byKey(editableTextKey);
|
||||
await tester.longPress(textFinder);
|
||||
tester.state<EditableTextState>(textFinder).showToolbar();
|
||||
await tester.pump();
|
||||
|
||||
await tester.tap(find.text('PASTE'));
|
||||
|
@ -478,6 +478,45 @@ void main() {
|
||||
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',
|
||||
(WidgetTester tester) async {
|
||||
final GlobalKey<EditableTextState> editableTextKey =
|
||||
@ -513,6 +552,7 @@ void main() {
|
||||
// Long-press to bring up the text editing controls.
|
||||
final Finder textFinder = find.byKey(editableTextKey);
|
||||
await tester.longPress(textFinder);
|
||||
tester.state<EditableTextState>(textFinder).showToolbar();
|
||||
await tester.pump();
|
||||
|
||||
await tester.tap(find.text('PASTE'));
|
||||
|
Loading…
x
Reference in New Issue
Block a user