Unverified Commit a40c5c29 authored by YeungKC's avatar YeungKC Committed by GitHub

Migration text selection manipulation. (#86986)

Consolidate duplicated cut/copy/paste/selectall code so it can be done via Actions in the future.
parent 4d4f3372
......@@ -2165,23 +2165,20 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
///
/// {@macro flutter.rendering.RenderEditable.cause}
void selectAll(SelectionChangedCause cause) {
_setSelection(
selection!.copyWith(
baseOffset: 0,
extentOffset: textSelectionDelegate.textEditingValue.text.length,
),
cause,
);
textSelectionDelegate.selectAll(cause);
}
/// Copy current [selection] to [Clipboard].
void copySelection() {
///
/// {@macro flutter.rendering.RenderEditable.cause}
void copySelection(SelectionChangedCause cause) {
final TextSelection selection = textSelectionDelegate.textEditingValue.selection;
final String text = textSelectionDelegate.textEditingValue.text;
assert(selection != null);
if (!selection.isCollapsed) {
Clipboard.setData(ClipboardData(text: selection.textInside(text)));
if (selection.isCollapsed) {
return;
}
textSelectionDelegate.copySelection(cause);
}
/// Cut current [selection] to Clipboard.
......@@ -2192,18 +2189,12 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
return;
}
final TextSelection selection = textSelectionDelegate.textEditingValue.selection;
final String text = textSelectionDelegate.textEditingValue.text;
assert(selection != null);
if (!selection.isCollapsed) {
Clipboard.setData(ClipboardData(text: selection.textInside(text)));
_setTextEditingValue(
TextEditingValue(
text: selection.textBefore(text) + selection.textAfter(text),
selection: TextSelection.collapsed(offset: math.min(selection.start, selection.end)),
),
cause,
);
if (selection.isCollapsed) {
return;
}
textSelectionDelegate.cutSelection(cause);
}
/// Paste text from [Clipboard].
......@@ -2216,22 +2207,12 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
return;
}
final TextSelection selection = textSelectionDelegate.textEditingValue.selection;
final String text = textSelectionDelegate.textEditingValue.text;
assert(selection != null);
// Snapshot the input before using `await`.
// See https://github.com/flutter/flutter/issues/11427
final ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain);
if (data != null && selection.isValid) {
_setTextEditingValue(
TextEditingValue(
text: selection.textBefore(text) + data.text! + selection.textAfter(text),
selection: TextSelection.collapsed(
offset: math.min(selection.start, selection.end) + data.text!.length,
),
),
cause,
);
if (!selection.isValid) {
return;
}
textSelectionDelegate.pasteText(cause);
}
@override
......
......@@ -17,6 +17,7 @@ import 'dart:ui' show
import 'package:flutter/foundation.dart';
import 'package:vector_math/vector_math_64.dart' show Matrix4;
import '../../services.dart' show Clipboard, ClipboardData;
import 'autofill.dart';
import 'message_codec.dart';
import 'platform_channel.dart';
......@@ -804,15 +805,15 @@ enum SelectionChangedCause {
/// location of the cursor.
///
/// An example is when the user taps on select all in the tool bar.
toolBar,
toolbar,
/// The user used the mouse to change the selection by dragging over a piece
/// of text.
drag,
}
/// A mixin for manipulating the selection, to be used by the implementer
/// of the toolbar widget.
/// A mixin for manipulating the selection, provided for toolbar or shortcut
/// keys.
mixin TextSelectionDelegate {
/// Gets the current text input.
TextEditingValue get textEditingValue;
......@@ -863,6 +864,117 @@ mixin TextSelectionDelegate {
/// Whether select all is enabled, must not be null.
bool get selectAllEnabled => true;
/// Cut current selection to [Clipboard].
///
/// If and only if [cause] is [SelectionChangedCause.toolbar], the toolbar
/// will be hidden and the current selection will be scrolled into view.
void cutSelection(SelectionChangedCause cause) {
final TextSelection selection = textEditingValue.selection;
final String text = textEditingValue.text;
Clipboard.setData(ClipboardData(
text: selection.textInside(text),
));
userUpdateTextEditingValue(
TextEditingValue(
text: selection.textBefore(text) + selection.textAfter(text),
selection: TextSelection.collapsed(
offset: selection.start,
),
),
cause,
);
if (cause == SelectionChangedCause.toolbar) {
bringIntoView(textEditingValue.selection.extent);
hideToolbar();
}
}
/// Paste text from [Clipboard].
///
/// If there is currently a selection, it will be replaced.
///
/// If and only if [cause] is [SelectionChangedCause.toolbar], the toolbar
/// will be hidden and the current selection will be scrolled into view.
Future<void> pasteText(SelectionChangedCause cause) async {
final TextEditingValue value = textEditingValue;
// Snapshot the input before using `await`.
// See https://github.com/flutter/flutter/issues/11427
final ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain);
if (data != null) {
userUpdateTextEditingValue(
TextEditingValue(
text: value.selection.textBefore(value.text)
+ data.text!
+ value.selection.textAfter(value.text),
selection: TextSelection.collapsed(
offset: value.selection.start + data.text!.length,
),
),
cause,
);
}
if (cause == SelectionChangedCause.toolbar) {
bringIntoView(textEditingValue.selection.extent);
hideToolbar();
}
}
/// Set the current selection to contain the entire text value.
///
/// If and only if [cause] is [SelectionChangedCause.toolbar], the selection
/// will be scrolled into view.
void selectAll(SelectionChangedCause cause) {
userUpdateTextEditingValue(
TextEditingValue(
text: textEditingValue.text,
selection: textEditingValue.selection.copyWith(
baseOffset: 0,
extentOffset: textEditingValue.text.length,
),
),
cause,
);
if (cause == SelectionChangedCause.toolbar) {
bringIntoView(textEditingValue.selection.extent);
}
}
/// Copy current selection to [Clipboard].
///
/// If [cause] is [SelectionChangedCause.toolbar], the position of
/// [bringIntoView] to selection will be called and hide toolbar.
void copySelection(SelectionChangedCause cause) {
final TextEditingValue value = textEditingValue;
Clipboard.setData(ClipboardData(
text: value.selection.textInside(value.text),
));
if (cause == SelectionChangedCause.toolbar) {
bringIntoView(textEditingValue.selection.extent);
hideToolbar(false);
switch (defaultTargetPlatform) {
case TargetPlatform.iOS:
break;
case TargetPlatform.macOS:
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
// Collapse the selection and hide the toolbar and handles.
userUpdateTextEditingValue(
TextEditingValue(
text: value.text,
selection: TextSelection.collapsed(offset: value.selection.end),
),
cause,
);
break;
}
}
}
}
/// An interface to receive information from [TextInput].
......
......@@ -307,7 +307,7 @@ class _SelectAllTextAction extends TextEditingAction<SelectAllTextIntent> {
class _CopySelectionTextAction extends TextEditingAction<CopySelectionTextIntent> {
@override
Object? invoke(CopySelectionTextIntent intent, [BuildContext? context]) {
textEditingActionTarget!.renderEditable.copySelection();
textEditingActionTarget!.renderEditable.copySelection(SelectionChangedCause.keyboard);
}
}
......
......@@ -200,74 +200,24 @@ abstract class TextSelectionControls {
return delegate.selectAllEnabled && delegate.textEditingValue.text.isNotEmpty && delegate.textEditingValue.selection.isCollapsed;
}
// TODO(justinmc): This and other methods should be ported to Actions and
// removed, along with their keyboard shortcut equivalents.
// https://github.com/flutter/flutter/issues/75004
/// Copy the current selection of the text field managed by the given
/// `delegate` to the [Clipboard]. Then, remove the selected text from the
/// text field and hide the toolbar.
/// Call [TextSelectionDelegate.cutSelection] to cut current selection.
///
/// This is called by subclasses when their cut affordance is activated by
/// the user.
void handleCut(TextSelectionDelegate delegate) {
final TextEditingValue value = delegate.textEditingValue;
Clipboard.setData(ClipboardData(
text: value.selection.textInside(value.text),
));
delegate.userUpdateTextEditingValue(
TextEditingValue(
text: value.selection.textBefore(value.text)
+ value.selection.textAfter(value.text),
selection: TextSelection.collapsed(
offset: value.selection.start,
),
),
SelectionChangedCause.toolBar,
);
delegate.bringIntoView(delegate.textEditingValue.selection.extent);
delegate.hideToolbar();
delegate.cutSelection(SelectionChangedCause.toolbar);
}
/// Copy the current selection of the text field managed by the given
/// `delegate` to the [Clipboard]. Then, move the cursor to the end of the
/// text (collapsing the selection in the process), and hide the toolbar.
/// Call [TextSelectionDelegate.copySelection] to copy current selection.
///
/// This is called by subclasses when their copy affordance is activated by
/// the user.
void handleCopy(TextSelectionDelegate delegate, ClipboardStatusNotifier? clipboardStatus) {
final TextEditingValue value = delegate.textEditingValue;
Clipboard.setData(ClipboardData(
text: value.selection.textInside(value.text),
));
delegate.copySelection(SelectionChangedCause.toolbar);
clipboardStatus?.update();
delegate.bringIntoView(delegate.textEditingValue.selection.extent);
switch (defaultTargetPlatform) {
case TargetPlatform.iOS:
// Hide the toolbar, but keep the selection and keep the handles.
delegate.hideToolbar(false);
return;
case TargetPlatform.macOS:
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
// Collapse the selection and hide the toolbar and handles.
delegate.userUpdateTextEditingValue(
TextEditingValue(
text: value.text,
selection: TextSelection.collapsed(offset: value.selection.end),
),
SelectionChangedCause.toolBar,
);
delegate.hideToolbar();
return;
}
}
/// Paste the current clipboard selection (obtained from [Clipboard]) into
/// the text field managed by the given `delegate`, replacing its current
/// selection, if any. Then, hide the toolbar.
/// Call [TextSelectionDelegate.pasteText] to paste text.
///
/// This is called by subclasses when their paste affordance is activated by
/// the user.
......@@ -277,44 +227,18 @@ abstract class TextSelectionControls {
/// implemented.
// TODO(ianh): https://github.com/flutter/flutter/issues/11427
Future<void> handlePaste(TextSelectionDelegate delegate) async {
final TextEditingValue value = delegate.textEditingValue; // Snapshot the input before using `await`.
final ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain);
if (data != null) {
delegate.userUpdateTextEditingValue(
TextEditingValue(
text: value.selection.textBefore(value.text)
+ data.text!
+ value.selection.textAfter(value.text),
selection: TextSelection.collapsed(
offset: value.selection.start + data.text!.length,
),
),
SelectionChangedCause.toolBar,
);
}
delegate.bringIntoView(delegate.textEditingValue.selection.extent);
delegate.hideToolbar();
delegate.pasteText(SelectionChangedCause.toolbar);
}
/// Adjust the selection of the text field managed by the given `delegate` so
/// that everything is selected.
/// Call [TextSelectionDelegate.selectAll] to set the current selection to
/// contain the entire text value.
///
/// Does not hide the toolbar.
///
/// This is called by subclasses when their select-all affordance is activated
/// by the user.
void handleSelectAll(TextSelectionDelegate delegate) {
delegate.userUpdateTextEditingValue(
TextEditingValue(
text: delegate.textEditingValue.text,
selection: TextSelection(
baseOffset: 0,
extentOffset: delegate.textEditingValue.text.length,
),
),
SelectionChangedCause.toolBar,
);
delegate.bringIntoView(delegate.textEditingValue.selection.extent);
delegate.selectAll(SelectionChangedCause.toolbar);
}
}
......
......@@ -8107,9 +8107,97 @@ void main() {
await tester.pump();
expect(fadeTransition.toString(), contains('DISPOSED'));
// On web, using keyboard for selection is handled by the browser.
}, skip: kIsWeb); // [intended]
testWidgets('Selection will be scrolled into view with SelectionChangedCause', (WidgetTester tester) async {
final GlobalKey<EditableTextState> key = GlobalKey<EditableTextState>();
final String text = List<int>.generate(64, (int index) => index).join('\n');
final TextEditingController controller = TextEditingController(text: text);
final ScrollController scrollController = ScrollController();
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: SizedBox(
height: 32,
child: EditableText(
key: key,
focusNode: focusNode,
style: Typography.material2018(platform: TargetPlatform.android).black.subtitle1!,
cursorColor: Colors.blue,
backgroundCursorColor: Colors.grey,
controller: controller,
scrollController: scrollController,
maxLines: 2,
),
),
),
),
),
);
final TextSelectionDelegate textSelectionDelegate = key.currentState!;
late double maxScrollExtent;
Future<void> resetSelectionAndScrollOffset([bool setMaxScrollExtent = true]) async {
controller.value = controller.value.copyWith(
text: text,
selection: controller.selection.copyWith(baseOffset: 0, extentOffset: 1),
);
await tester.pump();
final double targetOffset = setMaxScrollExtent ? scrollController.position.maxScrollExtent : 0.0;
scrollController.jumpTo(targetOffset);
await tester.pumpAndSettle();
maxScrollExtent = scrollController.position.maxScrollExtent;
expect(scrollController.offset, targetOffset);
}
// Cut
await resetSelectionAndScrollOffset();
textSelectionDelegate.cutSelection(SelectionChangedCause.keyboard);
await tester.pump();
expect(scrollController.offset, maxScrollExtent);
await resetSelectionAndScrollOffset();
textSelectionDelegate.cutSelection(SelectionChangedCause.toolbar);
await tester.pump();
expect(scrollController.offset.roundToDouble(), 0.0);
// Paste
await resetSelectionAndScrollOffset();
textSelectionDelegate.pasteText(SelectionChangedCause.keyboard);
await tester.pump();
expect(scrollController.offset, maxScrollExtent);
await resetSelectionAndScrollOffset();
textSelectionDelegate.pasteText(SelectionChangedCause.toolbar);
await tester.pump();
expect(scrollController.offset.roundToDouble(), 0.0);
// Select all
await resetSelectionAndScrollOffset(false);
textSelectionDelegate.selectAll(SelectionChangedCause.keyboard);
await tester.pump();
expect(scrollController.offset, 0.0);
await resetSelectionAndScrollOffset(false);
textSelectionDelegate.selectAll(SelectionChangedCause.toolbar);
await tester.pump();
expect(scrollController.offset.roundToDouble(), maxScrollExtent);
// Copy
await resetSelectionAndScrollOffset();
textSelectionDelegate.copySelection(SelectionChangedCause.keyboard);
await tester.pump();
expect(scrollController.offset, maxScrollExtent);
await resetSelectionAndScrollOffset();
textSelectionDelegate.copySelection(SelectionChangedCause.toolbar);
await tester.pump();
expect(scrollController.offset.roundToDouble(), 0.0);
});
}
class UnsettableController extends TextEditingController {
......
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