Unverified Commit ee30499a authored by xster's avatar xster Committed by GitHub

Stop using SelectionChangedCause internally to show the text selection toolbar (#27534)

parent c920cb7c
...@@ -456,15 +456,18 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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) {
if (widget.selectionEnabled) {
_renderEditable.selectWord(cause: SelectionChangedCause.doubleTap); _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 ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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() { ...@@ -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() { ...@@ -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() { ...@@ -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() { ...@@ -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'));
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment