mirror of
https://github.com/flutter/flutter
synced 2024-08-25 02:46:31 +00:00
Update TextSelectionOverlay (#142463)
Fixes a bug where changing parameters in EditableText that affect the selection overlay didn't update the overlay.
This commit is contained in:
parent
102f6394d6
commit
f1eeda7415
|
@ -23,8 +23,8 @@ const double _kSelectionHandleRadius = 6;
|
|||
const double _kArrowScreenPadding = 26.0;
|
||||
|
||||
/// Draws a single text selection handle with a bar and a ball.
|
||||
class _TextSelectionHandlePainter extends CustomPainter {
|
||||
const _TextSelectionHandlePainter(this.color);
|
||||
class _CupertinoTextSelectionHandlePainter extends CustomPainter {
|
||||
const _CupertinoTextSelectionHandlePainter(this.color);
|
||||
|
||||
final Color color;
|
||||
|
||||
|
@ -51,7 +51,7 @@ class _TextSelectionHandlePainter extends CustomPainter {
|
|||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(_TextSelectionHandlePainter oldPainter) => color != oldPainter.color;
|
||||
bool shouldRepaint(_CupertinoTextSelectionHandlePainter oldPainter) => color != oldPainter.color;
|
||||
}
|
||||
|
||||
/// iOS Cupertino styled text selection handle controls.
|
||||
|
@ -116,7 +116,7 @@ class CupertinoTextSelectionControls extends TextSelectionControls {
|
|||
final Widget handle;
|
||||
|
||||
final Widget customPaint = CustomPaint(
|
||||
painter: _TextSelectionHandlePainter(CupertinoTheme.of(context).primaryColor),
|
||||
painter: _CupertinoTextSelectionHandlePainter(CupertinoTheme.of(context).primaryColor),
|
||||
);
|
||||
|
||||
// [buildHandle]'s widget is positioned at the selection cursor's bottom
|
||||
|
|
|
@ -2928,7 +2928,28 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||
widget.controller.addListener(_didChangeTextEditingValue);
|
||||
_updateRemoteEditingValueIfNeeded();
|
||||
}
|
||||
if (widget.controller.selection != oldWidget.controller.selection) {
|
||||
|
||||
if (_selectionOverlay != null
|
||||
&& (widget.contextMenuBuilder != oldWidget.contextMenuBuilder ||
|
||||
widget.selectionControls != oldWidget.selectionControls ||
|
||||
widget.onSelectionHandleTapped != oldWidget.onSelectionHandleTapped ||
|
||||
widget.dragStartBehavior != oldWidget.dragStartBehavior ||
|
||||
widget.magnifierConfiguration != oldWidget.magnifierConfiguration)) {
|
||||
final bool shouldShowToolbar = _selectionOverlay!.toolbarIsVisible;
|
||||
final bool shouldShowHandles = _selectionOverlay!.handlesVisible;
|
||||
_selectionOverlay!.dispose();
|
||||
_selectionOverlay = _createSelectionOverlay();
|
||||
if (shouldShowToolbar || shouldShowHandles) {
|
||||
SchedulerBinding.instance.addPostFrameCallback((Duration _) {
|
||||
if (shouldShowToolbar) {
|
||||
_selectionOverlay!.showToolbar();
|
||||
}
|
||||
if (shouldShowHandles) {
|
||||
_selectionOverlay!.showHandles();
|
||||
}
|
||||
});
|
||||
}
|
||||
} else if (widget.controller.selection != oldWidget.controller.selection) {
|
||||
_selectionOverlay?.update(_value);
|
||||
}
|
||||
_selectionOverlay?.handlesVisible = widget.showSelectionHandles;
|
||||
|
@ -4266,7 +4287,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||
if (!widget.focusNode.hasFocus) {
|
||||
_flagInternalFocus();
|
||||
widget.focusNode.requestFocus();
|
||||
_selectionOverlay = _createSelectionOverlay();
|
||||
_selectionOverlay ??= _createSelectionOverlay();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -1509,13 +1509,7 @@ class SelectionOverlay {
|
|||
/// {@endtemplate}
|
||||
void hide() {
|
||||
_magnifierController.hide();
|
||||
if (_handles != null) {
|
||||
_handles!.start.remove();
|
||||
_handles!.start.dispose();
|
||||
_handles!.end.remove();
|
||||
_handles!.end.dispose();
|
||||
_handles = null;
|
||||
}
|
||||
hideHandles();
|
||||
if (_toolbar != null || _contextMenuController.isShown || _spellCheckToolbarController.isShown) {
|
||||
hideToolbar();
|
||||
}
|
||||
|
|
|
@ -15030,6 +15030,352 @@ void main() {
|
|||
skip: kIsWeb, // [intended] on web the browser handles the context menu.
|
||||
);
|
||||
|
||||
testWidgets('contextMenuBuilder can be updated to display a new menu', (WidgetTester tester) async {
|
||||
// Regression test for https://github.com/flutter/flutter/issues/142077.
|
||||
late StateSetter setState;
|
||||
final GlobalKey keyOne = GlobalKey();
|
||||
final GlobalKey keyTwo = GlobalKey();
|
||||
GlobalKey key = keyOne;
|
||||
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
home: Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: SizedBox(
|
||||
width: 400,
|
||||
child: StatefulBuilder(
|
||||
builder: (BuildContext context, StateSetter localSetState) {
|
||||
setState = localSetState;
|
||||
return EditableText(
|
||||
maxLines: 10,
|
||||
controller: controller,
|
||||
showSelectionHandles: true,
|
||||
autofocus: true,
|
||||
focusNode: focusNode,
|
||||
style: Typography.material2018().black.subtitle1!,
|
||||
cursorColor: Colors.blue,
|
||||
backgroundCursorColor: Colors.grey,
|
||||
keyboardType: TextInputType.text,
|
||||
textAlign: TextAlign.right,
|
||||
selectionControls: materialTextSelectionHandleControls,
|
||||
contextMenuBuilder: (
|
||||
BuildContext context,
|
||||
EditableTextState editableTextState,
|
||||
) {
|
||||
return SizedBox(
|
||||
key: key,
|
||||
width: 10.0,
|
||||
height: 10.0,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
));
|
||||
|
||||
await tester.pump(); // Wait for autofocus to take effect.
|
||||
|
||||
expect(find.byKey(keyOne), findsNothing);
|
||||
expect(find.byKey(keyTwo), findsNothing);
|
||||
|
||||
// Long-press to bring up the context menu.
|
||||
final Finder textFinder = find.byType(EditableText);
|
||||
await tester.longPress(textFinder);
|
||||
tester.state<EditableTextState>(textFinder).showToolbar();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byKey(keyOne), findsOneWidget);
|
||||
expect(find.byKey(keyTwo), findsNothing);
|
||||
|
||||
setState(() {
|
||||
key = keyTwo;
|
||||
});
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byKey(keyOne), findsNothing);
|
||||
expect(find.byKey(keyTwo), findsOneWidget);
|
||||
},
|
||||
skip: kIsWeb, // [intended] on web the browser handles the context menu.
|
||||
);
|
||||
|
||||
testWidgets('selectionControls can be updated', (WidgetTester tester) async {
|
||||
// Regression test for https://github.com/flutter/flutter/issues/142077.
|
||||
controller.text = 'test';
|
||||
late StateSetter setState;
|
||||
TextSelectionControls selectionControls = materialTextSelectionControls;
|
||||
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
home: Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: SizedBox(
|
||||
width: 400,
|
||||
child: StatefulBuilder(
|
||||
builder: (BuildContext context, StateSetter localSetState) {
|
||||
setState = localSetState;
|
||||
return EditableText(
|
||||
maxLines: 10,
|
||||
controller: controller,
|
||||
showSelectionHandles: true,
|
||||
autofocus: true,
|
||||
focusNode: focusNode,
|
||||
style: Typography.material2018().black.subtitle1!,
|
||||
cursorColor: Colors.blue,
|
||||
backgroundCursorColor: Colors.grey,
|
||||
keyboardType: TextInputType.text,
|
||||
textAlign: TextAlign.right,
|
||||
selectionControls: selectionControls,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
));
|
||||
|
||||
await tester.pump(); // Wait for autofocus to take effect.
|
||||
|
||||
final Finder materialHandleFinder = find.byWidgetPredicate((Widget widget) {
|
||||
if (widget.runtimeType != CustomPaint) {
|
||||
return false;
|
||||
}
|
||||
final CustomPaint customPaint = widget as CustomPaint;
|
||||
return '${customPaint.painter.runtimeType}' == '_TextSelectionHandlePainter';
|
||||
});
|
||||
final Finder cupertinoHandleFinder = find.byWidgetPredicate((Widget widget) {
|
||||
if (widget.runtimeType != CustomPaint) {
|
||||
return false;
|
||||
}
|
||||
final CustomPaint customPaint = widget as CustomPaint;
|
||||
return '${customPaint.painter.runtimeType}' == '_CupertinoTextSelectionHandlePainter';
|
||||
});
|
||||
expect(materialHandleFinder, findsOneWidget);
|
||||
expect(cupertinoHandleFinder, findsNothing);
|
||||
|
||||
// Long-press to select the text because Cupertino doesn't show a selection
|
||||
// handle when the selection is collapsed.
|
||||
final Finder textFinder = find.byType(EditableText);
|
||||
await tester.longPress(textFinder);
|
||||
tester.state<EditableTextState>(textFinder).showToolbar();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(materialHandleFinder, findsNWidgets(2));
|
||||
expect(cupertinoHandleFinder, findsNothing);
|
||||
|
||||
setState(() {
|
||||
selectionControls = cupertinoTextSelectionControls;
|
||||
});
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(materialHandleFinder, findsNothing);
|
||||
expect(cupertinoHandleFinder, findsNWidgets(2));
|
||||
},
|
||||
skip: kIsWeb, // [intended] on web the browser handles the context menu.
|
||||
);
|
||||
|
||||
testWidgets('onSelectionHandleTapped can be updated', (WidgetTester tester) async {
|
||||
// Regression test for https://github.com/flutter/flutter/issues/142077.
|
||||
late StateSetter setState;
|
||||
int tapCount = 0;
|
||||
VoidCallback? onSelectionHandleTapped;
|
||||
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
home: Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: SizedBox(
|
||||
width: 400,
|
||||
child: StatefulBuilder(
|
||||
builder: (BuildContext context, StateSetter localSetState) {
|
||||
setState = localSetState;
|
||||
return EditableText(
|
||||
maxLines: 10,
|
||||
controller: controller,
|
||||
showSelectionHandles: true,
|
||||
autofocus: true,
|
||||
focusNode: focusNode,
|
||||
style: Typography.material2018().black.subtitle1!,
|
||||
cursorColor: Colors.blue,
|
||||
backgroundCursorColor: Colors.grey,
|
||||
keyboardType: TextInputType.text,
|
||||
textAlign: TextAlign.right,
|
||||
selectionControls: materialTextSelectionControls,
|
||||
onSelectionHandleTapped: onSelectionHandleTapped,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
));
|
||||
|
||||
await tester.pump(); // Wait for autofocus to take effect.
|
||||
|
||||
final Finder materialHandleFinder = find.byWidgetPredicate((Widget widget) {
|
||||
if (widget.runtimeType != CustomPaint) {
|
||||
return false;
|
||||
}
|
||||
final CustomPaint customPaint = widget as CustomPaint;
|
||||
return '${customPaint.painter.runtimeType}' == '_TextSelectionHandlePainter';
|
||||
});
|
||||
expect(materialHandleFinder, findsOneWidget);
|
||||
expect(tapCount, equals(0));
|
||||
|
||||
await tester.tap(materialHandleFinder);
|
||||
await tester.pump();
|
||||
expect(tapCount, equals(0));
|
||||
|
||||
setState(() {
|
||||
onSelectionHandleTapped = () => tapCount += 1;
|
||||
});
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(materialHandleFinder);
|
||||
await tester.pump();
|
||||
expect(tapCount, equals(1));
|
||||
},
|
||||
skip: kIsWeb, // [intended] on web the browser handles the context menu.
|
||||
);
|
||||
|
||||
testWidgets('dragStartBehavior can be updated', (WidgetTester tester) async {
|
||||
// Regression test for https://github.com/flutter/flutter/issues/142077.
|
||||
late StateSetter setState;
|
||||
DragStartBehavior dragStartBehavior = DragStartBehavior.down;
|
||||
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
home: Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: SizedBox(
|
||||
width: 400,
|
||||
child: StatefulBuilder(
|
||||
builder: (BuildContext context, StateSetter localSetState) {
|
||||
setState = localSetState;
|
||||
return EditableText(
|
||||
maxLines: 10,
|
||||
controller: controller,
|
||||
showSelectionHandles: true,
|
||||
autofocus: true,
|
||||
focusNode: focusNode,
|
||||
style: Typography.material2018().black.subtitle1!,
|
||||
cursorColor: Colors.blue,
|
||||
backgroundCursorColor: Colors.grey,
|
||||
keyboardType: TextInputType.text,
|
||||
textAlign: TextAlign.right,
|
||||
selectionControls: materialTextSelectionControls,
|
||||
dragStartBehavior: dragStartBehavior,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
));
|
||||
|
||||
await tester.pump(); // Wait for autofocus to take effect.
|
||||
|
||||
final Finder handleOverlayFinder = find.descendant(
|
||||
of: find.byType(Overlay),
|
||||
matching: find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_SelectionHandleOverlay'),
|
||||
);
|
||||
expect(handleOverlayFinder, findsOneWidget);
|
||||
|
||||
// Expects that the selection handle has the given DragStartBehavior.
|
||||
void checkDragStartBehavior(DragStartBehavior dragStartBehavior) {
|
||||
final RawGestureDetector rawGestureDetector = tester.widget(find.descendant(
|
||||
of: handleOverlayFinder,
|
||||
matching: find.byType(RawGestureDetector)
|
||||
).first);
|
||||
final GestureRecognizerFactory<GestureRecognizer>? recognizerFactory = rawGestureDetector.gestures[PanGestureRecognizer];
|
||||
final PanGestureRecognizer recognizer = PanGestureRecognizer();
|
||||
recognizerFactory?.initializer(recognizer);
|
||||
expect(recognizer.dragStartBehavior, dragStartBehavior);
|
||||
recognizer.dispose();
|
||||
}
|
||||
|
||||
checkDragStartBehavior(DragStartBehavior.down);
|
||||
|
||||
setState(() {
|
||||
dragStartBehavior = DragStartBehavior.start;
|
||||
});
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(handleOverlayFinder, findsOneWidget);
|
||||
checkDragStartBehavior(DragStartBehavior.start);
|
||||
},
|
||||
skip: kIsWeb, // [intended] on web the browser handles the context menu.
|
||||
);
|
||||
|
||||
testWidgets('magnifierConfiguration can be updated to display a new magnifier', (WidgetTester tester) async {
|
||||
// Regression test for https://github.com/flutter/flutter/issues/142077.
|
||||
late StateSetter setState;
|
||||
final GlobalKey keyOne = GlobalKey();
|
||||
final GlobalKey keyTwo = GlobalKey();
|
||||
GlobalKey key = keyOne;
|
||||
|
||||
final TextMagnifierConfiguration magnifierConfiguration = TextMagnifierConfiguration(
|
||||
magnifierBuilder: (BuildContext context, MagnifierController controller, ValueNotifier<MagnifierInfo>? info) {
|
||||
return Placeholder(
|
||||
key: key,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
home: Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: SizedBox(
|
||||
width: 400,
|
||||
child: StatefulBuilder(
|
||||
builder: (BuildContext context, StateSetter localSetState) {
|
||||
setState = localSetState;
|
||||
return EditableText(
|
||||
maxLines: 10,
|
||||
controller: controller,
|
||||
showSelectionHandles: true,
|
||||
autofocus: true,
|
||||
focusNode: focusNode,
|
||||
style: Typography.material2018().black.subtitle1!,
|
||||
cursorColor: Colors.blue,
|
||||
backgroundCursorColor: Colors.grey,
|
||||
keyboardType: TextInputType.text,
|
||||
textAlign: TextAlign.right,
|
||||
selectionControls: materialTextSelectionHandleControls,
|
||||
magnifierConfiguration: magnifierConfiguration,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
));
|
||||
|
||||
await tester.pump(); // Wait for autofocus to take effect.
|
||||
|
||||
void checkMagnifierKey(Key testKey) {
|
||||
final EditableText editableText = tester.widget(find.byType(EditableText));
|
||||
final BuildContext context = tester.firstElement(find.byType(EditableText));
|
||||
final ValueNotifier<MagnifierInfo> magnifierInfo = ValueNotifier<MagnifierInfo>(MagnifierInfo.empty);
|
||||
expect(
|
||||
editableText.magnifierConfiguration.magnifierBuilder(
|
||||
context,
|
||||
MagnifierController(),
|
||||
magnifierInfo,
|
||||
),
|
||||
isA<Widget>().having(
|
||||
(Widget widget) => widget.key,
|
||||
'built magnifier key equal to passed in magnifier key',
|
||||
equals(testKey),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
checkMagnifierKey(keyOne);
|
||||
|
||||
setState(() {
|
||||
key = keyTwo;
|
||||
});
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
checkMagnifierKey(keyTwo);
|
||||
},
|
||||
skip: kIsWeb, // [intended] on web the browser handles the context menu.
|
||||
);
|
||||
|
||||
group('Spell check', () {
|
||||
testWidgets(
|
||||
'Spell check configured properly when spell check disabled by default',
|
||||
|
|
Loading…
Reference in a new issue