Unverified Commit 52c715fe authored by Jaime Blasco's avatar Jaime Blasco Committed by GitHub

Add textSelectionControls to TextField etc. (#66785)

Enables custom text selection menus by allowing selectionControls to be passed to TextField et. al.
parent 16029c38
......@@ -270,6 +270,7 @@ class CupertinoTextField extends StatefulWidget {
this.scrollPadding = const EdgeInsets.all(20.0),
this.dragStartBehavior = DragStartBehavior.start,
this.enableInteractiveSelection = true,
this.selectionControls,
this.onTap,
this.scrollController,
this.scrollPhysics,
......@@ -559,6 +560,9 @@ class CupertinoTextField extends StatefulWidget {
/// {@macro flutter.widgets.editableText.enableInteractiveSelection}
final bool enableInteractiveSelection;
/// {@macro flutter.widgets.editableText.selectionControls}
final TextSelectionControls? selectionControls;
/// {@macro flutter.widgets.scrollable.dragStartBehavior}
final DragStartBehavior dragStartBehavior;
......@@ -615,6 +619,7 @@ class CupertinoTextField extends StatefulWidget {
properties.add(DiagnosticsProperty<Radius>('cursorRadius', cursorRadius, defaultValue: null));
properties.add(createCupertinoColorProperty('cursorColor', cursorColor, defaultValue: null));
properties.add(FlagProperty('selectionEnabled', value: selectionEnabled, defaultValue: true, ifFalse: 'selection disabled'));
properties.add(DiagnosticsProperty<TextSelectionControls>('selectionControls', selectionControls, defaultValue: null));
properties.add(DiagnosticsProperty<ScrollController>('scrollController', scrollController, defaultValue: null));
properties.add(DiagnosticsProperty<ScrollPhysics>('scrollPhysics', scrollPhysics, defaultValue: null));
properties.add(EnumProperty<TextAlign>('textAlign', textAlign, defaultValue: TextAlign.start));
......@@ -874,6 +879,7 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with Restoratio
assert(debugCheckHasDirectionality(context));
final TextEditingController controller = _effectiveController;
final List<TextInputFormatter> formatters = widget.inputFormatters ?? <TextInputFormatter>[];
final TextSelectionControls textSelectionControls = widget.selectionControls ?? cupertinoTextSelectionControls;
final bool enabled = widget.enabled ?? true;
final Offset cursorOffset = Offset(_iOSHorizontalCursorOffsetPixels / MediaQuery.of(context)!.devicePixelRatio, 0);
if (widget.maxLength != null && widget.maxLengthEnforced) {
......@@ -957,7 +963,7 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with Restoratio
expands: widget.expands,
selectionColor: selectionColor,
selectionControls: widget.selectionEnabled
? cupertinoTextSelectionControls : null,
? textSelectionControls : null,
onChanged: widget.onChanged,
onSelectionChanged: _handleSelectionChanged,
onEditingComplete: widget.onEditingComplete,
......
......@@ -193,6 +193,7 @@ class SelectableText extends StatefulWidget {
this.cursorColor,
this.dragStartBehavior = DragStartBehavior.start,
this.enableInteractiveSelection = true,
this.selectionControls,
this.onTap,
this.scrollPhysics,
this.textHeightBehavior,
......@@ -245,6 +246,7 @@ class SelectableText extends StatefulWidget {
this.cursorColor,
this.dragStartBehavior = DragStartBehavior.start,
this.enableInteractiveSelection = true,
this.selectionControls,
this.onTap,
this.scrollPhysics,
this.textHeightBehavior,
......@@ -353,6 +355,9 @@ class SelectableText extends StatefulWidget {
/// {@macro flutter.widgets.editableText.enableInteractiveSelection}
final bool enableInteractiveSelection;
/// {@macro flutter.widgets.editableText.selectionControls}
final TextSelectionControls? selectionControls;
/// {@macro flutter.widgets.scrollable.dragStartBehavior}
final DragStartBehavior dragStartBehavior;
......@@ -416,6 +421,7 @@ class SelectableText extends StatefulWidget {
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<TextSelectionControls>('selectionControls', selectionControls, defaultValue: null));
properties.add(DiagnosticsProperty<ScrollPhysics>('scrollPhysics', scrollPhysics, defaultValue: null));
properties.add(DiagnosticsProperty<TextHeightBehavior>('textHeightBehavior', textHeightBehavior, defaultValue: null));
}
......@@ -565,7 +571,7 @@ class _SelectableTextState extends State<SelectableText> with AutomaticKeepAlive
final TextSelectionThemeData selectionTheme = TextSelectionTheme.of(context);
final FocusNode focusNode = _effectiveFocusNode;
final TextSelectionControls textSelectionControls;
TextSelectionControls? textSelectionControls = widget.selectionControls;
final bool paintCursorAboveText;
final bool cursorOpacityAnimates;
Offset? cursorOffset;
......@@ -578,7 +584,7 @@ class _SelectableTextState extends State<SelectableText> with AutomaticKeepAlive
case TargetPlatform.macOS:
final CupertinoThemeData cupertinoTheme = CupertinoTheme.of(context);
forcePressEnabled = true;
textSelectionControls = cupertinoTextSelectionControls;
textSelectionControls ??= cupertinoTextSelectionControls;
paintCursorAboveText = true;
cursorOpacityAnimates = true;
cursorColor ??= selectionTheme.cursorColor ?? cupertinoTheme.primaryColor;
......@@ -592,7 +598,7 @@ class _SelectableTextState extends State<SelectableText> with AutomaticKeepAlive
case TargetPlatform.linux:
case TargetPlatform.windows:
forcePressEnabled = false;
textSelectionControls = materialTextSelectionControls;
textSelectionControls ??= materialTextSelectionControls;
paintCursorAboveText = false;
cursorOpacityAnimates = false;
cursorColor ??= selectionTheme.cursorColor ?? theme.colorScheme.primary;
......
......@@ -372,6 +372,7 @@ class TextField extends StatefulWidget {
this.scrollPadding = const EdgeInsets.all(20.0),
this.dragStartBehavior = DragStartBehavior.start,
this.enableInteractiveSelection = true,
this.selectionControls,
this.onTap,
this.mouseCursor,
this.buildCounter,
......@@ -674,6 +675,9 @@ class TextField extends StatefulWidget {
/// {@macro flutter.widgets.editableText.enableInteractiveSelection}
final bool enableInteractiveSelection;
/// {@macro flutter.widgets.editableText.selectionControls}
final TextSelectionControls? selectionControls;
/// {@macro flutter.widgets.scrollable.dragStartBehavior}
final DragStartBehavior dragStartBehavior;
......@@ -818,6 +822,7 @@ class TextField extends StatefulWidget {
properties.add(DiagnosticsProperty<Brightness>('keyboardAppearance', keyboardAppearance, defaultValue: null));
properties.add(DiagnosticsProperty<EdgeInsetsGeometry>('scrollPadding', scrollPadding, defaultValue: const EdgeInsets.all(20.0)));
properties.add(FlagProperty('selectionEnabled', value: selectionEnabled, defaultValue: true, ifFalse: 'selection disabled'));
properties.add(DiagnosticsProperty<TextSelectionControls>('selectionControls', selectionControls, defaultValue: null));
properties.add(DiagnosticsProperty<ScrollController>('scrollController', scrollController, defaultValue: null));
properties.add(DiagnosticsProperty<ScrollPhysics>('scrollPhysics', scrollPhysics, defaultValue: null));
}
......@@ -1092,7 +1097,7 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements
if (widget.maxLength != null && widget.maxLengthEnforced)
formatters.add(LengthLimitingTextInputFormatter(widget.maxLength));
final TextSelectionControls textSelectionControls;
TextSelectionControls? textSelectionControls = widget.selectionControls;
final bool paintCursorAboveText;
final bool cursorOpacityAnimates;
Offset? cursorOffset;
......@@ -1106,7 +1111,7 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements
case TargetPlatform.macOS:
final CupertinoThemeData cupertinoTheme = CupertinoTheme.of(context);
forcePressEnabled = true;
textSelectionControls = cupertinoTextSelectionControls;
textSelectionControls ??= cupertinoTextSelectionControls;
paintCursorAboveText = true;
cursorOpacityAnimates = true;
cursorColor ??= selectionTheme.cursorColor ?? cupertinoTheme.primaryColor;
......@@ -1121,7 +1126,7 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements
case TargetPlatform.linux:
case TargetPlatform.windows:
forcePressEnabled = false;
textSelectionControls = materialTextSelectionControls;
textSelectionControls ??= materialTextSelectionControls;
paintCursorAboveText = false;
cursorOpacityAnimates = false;
cursorColor ??= selectionTheme.cursorColor ?? theme.colorScheme.primary;
......
......@@ -182,6 +182,7 @@ class TextFormField extends FormField<String> {
Brightness? keyboardAppearance,
EdgeInsets scrollPadding = const EdgeInsets.all(20.0),
bool enableInteractiveSelection = true,
TextSelectionControls? selectionControls,
InputCounterWidgetBuilder? buildCounter,
ScrollPhysics? scrollPhysics,
Iterable<String>? autofillHints,
......@@ -276,6 +277,7 @@ class TextFormField extends FormField<String> {
scrollPhysics: scrollPhysics,
keyboardAppearance: keyboardAppearance,
enableInteractiveSelection: enableInteractiveSelection,
selectionControls: selectionControls,
buildCounter: buildCounter,
autofillHints: autofillHints,
);
......
......@@ -875,6 +875,7 @@ class EditableText extends StatefulWidget {
/// value is set to the ambient [ThemeData.textSelectionColor].
final Color? selectionColor;
/// {@template flutter.widgets.editableText.selectionControls}
/// Optional delegate for building the text selection handles and toolbar.
///
/// The [EditableText] widget used on its own will not trigger the display
......@@ -889,6 +890,7 @@ class EditableText extends StatefulWidget {
/// * [TextField], a Material Design themed wrapper of [EditableText], which
/// shows the selection toolbar upon appropriate user events based on the
/// user's platform set in [ThemeData.platform].
/// {@endtemplate}
final TextSelectionControls? selectionControls;
/// {@template flutter.widgets.editableText.keyboardType}
......
......@@ -29,6 +29,36 @@ class MockClipboard {
}
}
class MockTextSelectionControls extends TextSelectionControls {
@override
Widget buildHandle(BuildContext context, TextSelectionHandleType type,
double textLineHeight) {
throw UnimplementedError();
}
@override
Widget buildToolbar(
BuildContext context,
Rect globalEditableRegion,
double textLineHeight,
Offset position,
List<TextSelectionPoint> endpoints,
TextSelectionDelegate delegate,
ClipboardStatusNotifier clipboardStatus) {
throw UnimplementedError();
}
@override
Offset getHandleAnchor(TextSelectionHandleType type, double textLineHeight) {
throw UnimplementedError();
}
@override
Size getHandleSize(double textLineHeight) {
throw UnimplementedError();
}
}
class PathBoundsMatcher extends Matcher {
const PathBoundsMatcher({
this.rectMatcher,
......@@ -4123,4 +4153,20 @@ void main() {
matchesGoldenFile('text_field_golden.TextSelectionStyle.2.png'),
);
});
testWidgets('textSelectionControls is passed to EditableText', (WidgetTester tester) async {
final MockTextSelectionControls selectionControl = MockTextSelectionControls();
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(
selectionControls: selectionControl
),
),
),
);
final EditableText widget = tester.widget(find.byType(EditableText));
expect(widget.selectionControls, equals(selectionControl));
});
}
......@@ -6295,6 +6295,84 @@ void main() {
expect(find.byType(TextButton), findsNWidgets(4));
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.linux, TargetPlatform.windows }));
testWidgets('Custom toolbar test - Android text selection controls', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure',
);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: TextField(
controller: controller,
selectionControls: materialTextSelectionControls
),
),
),
),
);
final Offset textfieldStart = tester.getTopLeft(find.byType(TextField));
await tester.tapAt(textfieldStart + const Offset(150.0, 9.0));
await tester.pump(const Duration(milliseconds: 50));
await tester.tapAt(textfieldStart + const Offset(150.0, 9.0));
await tester.pumpAndSettle();
// Selected text shows 4 toolbar buttons: cut, copy, paste, select all
expect(find.byType(TextButton), findsNWidgets(4));
}, variant: TargetPlatformVariant.all());
testWidgets(
'Custom toolbar test - Cupertino text selection controls',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure',
);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: TextField(
controller: controller,
selectionControls: cupertinoTextSelectionControls,
),
),
),
),
);
final Offset textfieldStart = tester.getTopLeft(find.byType(TextField));
await tester.tapAt(textfieldStart + const Offset(150.0, 9.0));
await tester.pump(const Duration(milliseconds: 50));
await tester.tapAt(textfieldStart + const Offset(150.0, 9.0));
await tester.pumpAndSettle();
// Selected text shows 3 toolbar buttons: cut, copy, paste
expect(find.byType(CupertinoButton), findsNWidgets(3));
}, variant: TargetPlatformVariant.all());
testWidgets('selectionControls is passed to EditableText',
(WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Scaffold(
body: TextField(
selectionControls: materialTextSelectionControls,
),
),
),
),
);
final EditableText widget = tester.widget(find.byType(EditableText));
expect(widget.selectionControls, equals(materialTextSelectionControls));
});
testWidgets(
'double tap on top of cursor also selects word',
(WidgetTester tester) async {
......
......@@ -514,4 +514,21 @@ void main() {
);
}, throwsAssertionError);
});
testWidgets('textSelectionControls is passed to super', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Scaffold(
body: TextFormField(
selectionControls: materialTextSelectionControls,
),
),
),
),
);
final TextField widget = tester.widget(find.byType(TextField));
expect(widget.selectionControls, equals(materialTextSelectionControls));
});
}
......@@ -2771,6 +2771,93 @@ void main() {
},
);
testWidgets(
'long press selects word and shows custom toolbar (Android)',
(WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: SelectableText('Atwater Peel Sherbrooke Bonaventure',
selectionControls: cupertinoTextSelectionControls,
),
),
),
),
);
final Offset selectableTextStart = tester.getTopLeft(find.byType(SelectableText));
await tester.longPressAt(selectableTextStart + const Offset(50.0, 5.0));
await tester.pump();
final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first);
final TextEditingController controller = editableTextWidget.controller;
// The longpressed word is selected.
expect(
controller.selection,
const TextSelection(
baseOffset: 0,
extentOffset: 7,
),
);
// Toolbar shows one button.
expect(find.byType(CupertinoButton), findsNWidgets(1));
}, variant: TargetPlatformVariant.all());
testWidgets(
'long press selects word and shows custom toolbar (iOS)',
(WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: SelectableText('Atwater Peel Sherbrooke Bonaventure',
selectionControls: materialTextSelectionControls,
),
),
),
),
);
final Offset selectableTextStart = tester.getTopLeft(find.byType(SelectableText));
await tester.longPressAt(selectableTextStart + const Offset(50.0, 5.0));
await tester.pump();
final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first);
final TextEditingController controller = editableTextWidget.controller;
expect(
controller.selection,
const TextSelection(baseOffset: 0, extentOffset: 7),
);
// Collapsed toolbar shows 2 buttons: copy, select all
expect(find.byType(TextButton), findsNWidgets(2));
}, variant: TargetPlatformVariant.all());
testWidgets('textSelectionControls is passed to EditableText',
(WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Scaffold(
body: SelectableText('Atwater Peel Sherbrooke Bonaventure',
selectionControls: materialTextSelectionControls,
),
),
),
),
);
final EditableText widget = tester.widget(find.byType(EditableText));
expect(widget.selectionControls, equals(materialTextSelectionControls));
});
testWidgets(
'long press tap cannot initiate a double tap',
(WidgetTester tester) async {
......
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