mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
Bring back paste button hide behavior (#56689)
Unreverts #54902 with fixes for a failing integration test.
This commit is contained in:
parent
98bc176627
commit
d56349822d
@ -101,7 +101,6 @@ void main() {
|
|||||||
actions: <AndroidSemanticsAction>[
|
actions: <AndroidSemanticsAction>[
|
||||||
AndroidSemanticsAction.clearAccessibilityFocus,
|
AndroidSemanticsAction.clearAccessibilityFocus,
|
||||||
AndroidSemanticsAction.click,
|
AndroidSemanticsAction.click,
|
||||||
AndroidSemanticsAction.copy,
|
|
||||||
AndroidSemanticsAction.setSelection,
|
AndroidSemanticsAction.setSelection,
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -111,6 +110,33 @@ void main() {
|
|||||||
// Delay for TalkBack to update focus as of November 2019 with Pixel 3 and Android API 28
|
// Delay for TalkBack to update focus as of November 2019 with Pixel 3 and Android API 28
|
||||||
await Future<void>.delayed(const Duration(milliseconds: 500));
|
await Future<void>.delayed(const Duration(milliseconds: 500));
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await getSemantics(normalTextField),
|
||||||
|
hasAndroidSemantics(
|
||||||
|
text: 'hello world',
|
||||||
|
className: AndroidClassName.editText,
|
||||||
|
isFocusable: true,
|
||||||
|
isFocused: true,
|
||||||
|
isEditable: true,
|
||||||
|
isPassword: false,
|
||||||
|
actions: <AndroidSemanticsAction>[
|
||||||
|
AndroidSemanticsAction.clearAccessibilityFocus,
|
||||||
|
AndroidSemanticsAction.click,
|
||||||
|
AndroidSemanticsAction.setSelection,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Copy the text so that the clipboard contains something pasteable.
|
||||||
|
await driver.tap(normalTextField);
|
||||||
|
await Future<void>.delayed(const Duration(milliseconds: 50));
|
||||||
|
await driver.tap(normalTextField);
|
||||||
|
await Future<void>.delayed(const Duration(milliseconds: 500));
|
||||||
|
await driver.tap(find.text('SELECT ALL'));
|
||||||
|
await Future<void>.delayed(const Duration(milliseconds: 500));
|
||||||
|
await driver.tap(find.text('COPY'));
|
||||||
|
await Future<void>.delayed(const Duration(milliseconds: 50));
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
await getSemantics(normalTextField),
|
await getSemantics(normalTextField),
|
||||||
hasAndroidSemantics(
|
hasAndroidSemantics(
|
||||||
@ -124,6 +150,7 @@ void main() {
|
|||||||
AndroidSemanticsAction.clearAccessibilityFocus,
|
AndroidSemanticsAction.clearAccessibilityFocus,
|
||||||
AndroidSemanticsAction.click,
|
AndroidSemanticsAction.click,
|
||||||
AndroidSemanticsAction.copy,
|
AndroidSemanticsAction.copy,
|
||||||
|
AndroidSemanticsAction.nextAtMovementGranularity,
|
||||||
AndroidSemanticsAction.setSelection,
|
AndroidSemanticsAction.setSelection,
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -166,7 +193,6 @@ void main() {
|
|||||||
actions: <AndroidSemanticsAction>[
|
actions: <AndroidSemanticsAction>[
|
||||||
AndroidSemanticsAction.clearAccessibilityFocus,
|
AndroidSemanticsAction.clearAccessibilityFocus,
|
||||||
AndroidSemanticsAction.click,
|
AndroidSemanticsAction.click,
|
||||||
AndroidSemanticsAction.copy,
|
|
||||||
AndroidSemanticsAction.setSelection,
|
AndroidSemanticsAction.setSelection,
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -188,7 +214,6 @@ void main() {
|
|||||||
actions: <AndroidSemanticsAction>[
|
actions: <AndroidSemanticsAction>[
|
||||||
AndroidSemanticsAction.clearAccessibilityFocus,
|
AndroidSemanticsAction.clearAccessibilityFocus,
|
||||||
AndroidSemanticsAction.click,
|
AndroidSemanticsAction.click,
|
||||||
AndroidSemanticsAction.copy,
|
|
||||||
AndroidSemanticsAction.setSelection,
|
AndroidSemanticsAction.setSelection,
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -8,6 +8,7 @@ import 'dart:ui' as ui;
|
|||||||
|
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
import 'button.dart';
|
import 'button.dart';
|
||||||
import 'colors.dart';
|
import 'colors.dart';
|
||||||
@ -62,6 +63,151 @@ const TextStyle _kToolbarButtonDisabledFontStyle = TextStyle(
|
|||||||
// Eyeballed value.
|
// Eyeballed value.
|
||||||
const EdgeInsets _kToolbarButtonPadding = EdgeInsets.symmetric(vertical: 10.0, horizontal: 18.0);
|
const EdgeInsets _kToolbarButtonPadding = EdgeInsets.symmetric(vertical: 10.0, horizontal: 18.0);
|
||||||
|
|
||||||
|
// Generates the child that's passed into CupertinoTextSelectionToolbar.
|
||||||
|
class _CupertinoTextSelectionToolbarWrapper extends StatefulWidget {
|
||||||
|
const _CupertinoTextSelectionToolbarWrapper({
|
||||||
|
Key key,
|
||||||
|
this.arrowTipX,
|
||||||
|
this.barTopY,
|
||||||
|
this.clipboardStatus,
|
||||||
|
this.handleCut,
|
||||||
|
this.handleCopy,
|
||||||
|
this.handlePaste,
|
||||||
|
this.handleSelectAll,
|
||||||
|
this.isArrowPointingDown,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
final double arrowTipX;
|
||||||
|
final double barTopY;
|
||||||
|
final ClipboardStatusNotifier clipboardStatus;
|
||||||
|
final VoidCallback handleCut;
|
||||||
|
final VoidCallback handleCopy;
|
||||||
|
final VoidCallback handlePaste;
|
||||||
|
final VoidCallback handleSelectAll;
|
||||||
|
final bool isArrowPointingDown;
|
||||||
|
|
||||||
|
@override
|
||||||
|
_CupertinoTextSelectionToolbarWrapperState createState() => _CupertinoTextSelectionToolbarWrapperState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CupertinoTextSelectionToolbarWrapperState extends State<_CupertinoTextSelectionToolbarWrapper> {
|
||||||
|
ClipboardStatusNotifier _clipboardStatus;
|
||||||
|
|
||||||
|
void _onChangedClipboardStatus() {
|
||||||
|
setState(() {
|
||||||
|
// Inform the widget that the value of clipboardStatus has changed.
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_clipboardStatus = widget.clipboardStatus ?? ClipboardStatusNotifier();
|
||||||
|
_clipboardStatus.addListener(_onChangedClipboardStatus);
|
||||||
|
_clipboardStatus.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(_CupertinoTextSelectionToolbarWrapper oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
if (oldWidget.clipboardStatus == null && widget.clipboardStatus != null) {
|
||||||
|
_clipboardStatus.removeListener(_onChangedClipboardStatus);
|
||||||
|
_clipboardStatus.dispose();
|
||||||
|
_clipboardStatus = widget.clipboardStatus;
|
||||||
|
} else if (oldWidget.clipboardStatus != null) {
|
||||||
|
if (widget.clipboardStatus == null) {
|
||||||
|
_clipboardStatus = ClipboardStatusNotifier();
|
||||||
|
_clipboardStatus.addListener(_onChangedClipboardStatus);
|
||||||
|
oldWidget.clipboardStatus.removeListener(_onChangedClipboardStatus);
|
||||||
|
} else if (widget.clipboardStatus != oldWidget.clipboardStatus) {
|
||||||
|
_clipboardStatus = widget.clipboardStatus;
|
||||||
|
_clipboardStatus.addListener(_onChangedClipboardStatus);
|
||||||
|
oldWidget.clipboardStatus.removeListener(_onChangedClipboardStatus);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (widget.handlePaste != null) {
|
||||||
|
_clipboardStatus.update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
super.dispose();
|
||||||
|
// When used in an Overlay, this can be disposed after its creator has
|
||||||
|
// already disposed _clipboardStatus.
|
||||||
|
if (!_clipboardStatus.disposed) {
|
||||||
|
_clipboardStatus.removeListener(_onChangedClipboardStatus);
|
||||||
|
if (widget.clipboardStatus == null) {
|
||||||
|
_clipboardStatus.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
// Don't render the menu until the state of the clipboard is known.
|
||||||
|
if (widget.handlePaste != null
|
||||||
|
&& _clipboardStatus.value == ClipboardStatus.unknown) {
|
||||||
|
return const SizedBox(width: 0.0, height: 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<Widget> items = <Widget>[];
|
||||||
|
final CupertinoLocalizations localizations = CupertinoLocalizations.of(context);
|
||||||
|
final EdgeInsets arrowPadding = widget.isArrowPointingDown
|
||||||
|
? EdgeInsets.only(bottom: _kToolbarArrowSize.height)
|
||||||
|
: EdgeInsets.only(top: _kToolbarArrowSize.height);
|
||||||
|
final Widget onePhysicalPixelVerticalDivider =
|
||||||
|
SizedBox(width: 1.0 / MediaQuery.of(context).devicePixelRatio);
|
||||||
|
|
||||||
|
void addToolbarButton(
|
||||||
|
String text,
|
||||||
|
VoidCallback onPressed,
|
||||||
|
) {
|
||||||
|
if (items.isNotEmpty) {
|
||||||
|
items.add(onePhysicalPixelVerticalDivider);
|
||||||
|
}
|
||||||
|
|
||||||
|
items.add(CupertinoButton(
|
||||||
|
child: Text(
|
||||||
|
text,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: _kToolbarButtonFontStyle,
|
||||||
|
),
|
||||||
|
borderRadius: null,
|
||||||
|
color: _kToolbarBackgroundColor,
|
||||||
|
minSize: _kToolbarHeight,
|
||||||
|
onPressed: onPressed,
|
||||||
|
padding: _kToolbarButtonPadding.add(arrowPadding),
|
||||||
|
pressedOpacity: 0.7,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (widget.handleCut != null) {
|
||||||
|
addToolbarButton(localizations.cutButtonLabel, widget.handleCut);
|
||||||
|
}
|
||||||
|
if (widget.handleCopy != null) {
|
||||||
|
addToolbarButton(localizations.copyButtonLabel, widget.handleCopy);
|
||||||
|
}
|
||||||
|
if (widget.handlePaste != null
|
||||||
|
&& _clipboardStatus.value == ClipboardStatus.pasteable) {
|
||||||
|
addToolbarButton(localizations.pasteButtonLabel, widget.handlePaste);
|
||||||
|
}
|
||||||
|
if (widget.handleSelectAll != null) {
|
||||||
|
addToolbarButton(localizations.selectAllButtonLabel, widget.handleSelectAll);
|
||||||
|
}
|
||||||
|
|
||||||
|
return CupertinoTextSelectionToolbar._(
|
||||||
|
barTopY: widget.barTopY,
|
||||||
|
arrowTipX: widget.arrowTipX,
|
||||||
|
isArrowPointingDown: widget.isArrowPointingDown,
|
||||||
|
child: items.isEmpty ? null : _CupertinoTextSelectionToolbarContent(
|
||||||
|
isArrowPointingDown: widget.isArrowPointingDown,
|
||||||
|
children: items,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// An iOS-style toolbar that appears in response to text selection.
|
/// An iOS-style toolbar that appears in response to text selection.
|
||||||
///
|
///
|
||||||
/// Typically displays buttons for text manipulation, e.g. copying and pasting text.
|
/// Typically displays buttons for text manipulation, e.g. copying and pasting text.
|
||||||
@ -312,6 +458,7 @@ class _CupertinoTextSelectionControls extends TextSelectionControls {
|
|||||||
Offset position,
|
Offset position,
|
||||||
List<TextSelectionPoint> endpoints,
|
List<TextSelectionPoint> endpoints,
|
||||||
TextSelectionDelegate delegate,
|
TextSelectionDelegate delegate,
|
||||||
|
ClipboardStatusNotifier clipboardStatus,
|
||||||
) {
|
) {
|
||||||
assert(debugCheckHasMediaQuery(context));
|
assert(debugCheckHasMediaQuery(context));
|
||||||
final MediaQueryData mediaQuery = MediaQuery.of(context);
|
final MediaQueryData mediaQuery = MediaQuery.of(context);
|
||||||
@ -338,49 +485,15 @@ class _CupertinoTextSelectionControls extends TextSelectionControls {
|
|||||||
? endpoints.first.point.dy - textLineHeight - _kToolbarContentDistance - _kToolbarHeight
|
? endpoints.first.point.dy - textLineHeight - _kToolbarContentDistance - _kToolbarHeight
|
||||||
: endpoints.last.point.dy + _kToolbarContentDistance;
|
: endpoints.last.point.dy + _kToolbarContentDistance;
|
||||||
|
|
||||||
final List<Widget> items = <Widget>[];
|
return _CupertinoTextSelectionToolbarWrapper(
|
||||||
final CupertinoLocalizations localizations = CupertinoLocalizations.of(context);
|
|
||||||
final EdgeInsets arrowPadding = isArrowPointingDown
|
|
||||||
? EdgeInsets.only(bottom: _kToolbarArrowSize.height)
|
|
||||||
: EdgeInsets.only(top: _kToolbarArrowSize.height);
|
|
||||||
|
|
||||||
void addToolbarButtonIfNeeded(
|
|
||||||
String text,
|
|
||||||
bool Function(TextSelectionDelegate) predicate,
|
|
||||||
void Function(TextSelectionDelegate) onPressed,
|
|
||||||
) {
|
|
||||||
if (!predicate(delegate)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
items.add(CupertinoButton(
|
|
||||||
child: Text(
|
|
||||||
text,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
style: _kToolbarButtonFontStyle,
|
|
||||||
),
|
|
||||||
color: _kToolbarBackgroundColor,
|
|
||||||
minSize: _kToolbarHeight,
|
|
||||||
padding: _kToolbarButtonPadding.add(arrowPadding),
|
|
||||||
borderRadius: null,
|
|
||||||
pressedOpacity: 0.7,
|
|
||||||
onPressed: () => onPressed(delegate),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
addToolbarButtonIfNeeded(localizations.cutButtonLabel, canCut, handleCut);
|
|
||||||
addToolbarButtonIfNeeded(localizations.copyButtonLabel, canCopy, handleCopy);
|
|
||||||
addToolbarButtonIfNeeded(localizations.pasteButtonLabel, canPaste, handlePaste);
|
|
||||||
addToolbarButtonIfNeeded(localizations.selectAllButtonLabel, canSelectAll, handleSelectAll);
|
|
||||||
|
|
||||||
return CupertinoTextSelectionToolbar._(
|
|
||||||
barTopY: localBarTopY + globalEditableRegion.top,
|
|
||||||
arrowTipX: arrowTipX,
|
arrowTipX: arrowTipX,
|
||||||
|
barTopY: localBarTopY + globalEditableRegion.top,
|
||||||
|
clipboardStatus: clipboardStatus,
|
||||||
|
handleCut: canCut(delegate) ? () => handleCut(delegate) : null,
|
||||||
|
handleCopy: canCopy(delegate) ? () => handleCopy(delegate, clipboardStatus) : null,
|
||||||
|
handlePaste: canPaste(delegate) ? () => handlePaste(delegate) : null,
|
||||||
|
handleSelectAll: canSelectAll(delegate) ? () => handleSelectAll(delegate) : null,
|
||||||
isArrowPointingDown: isArrowPointingDown,
|
isArrowPointingDown: isArrowPointingDown,
|
||||||
child: items.isEmpty ? null : _CupertinoTextSelectionToolbarContent(
|
|
||||||
isArrowPointingDown: isArrowPointingDown,
|
|
||||||
children: items,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,6 +6,7 @@ import 'dart:math' as math;
|
|||||||
|
|
||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
import 'package:flutter/scheduler.dart';
|
import 'package:flutter/scheduler.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
import 'debug.dart';
|
import 'debug.dart';
|
||||||
@ -29,6 +30,7 @@ const double _kToolbarContentDistance = 8.0;
|
|||||||
/// Manages a copy/paste text selection toolbar.
|
/// Manages a copy/paste text selection toolbar.
|
||||||
class _TextSelectionToolbar extends StatefulWidget {
|
class _TextSelectionToolbar extends StatefulWidget {
|
||||||
const _TextSelectionToolbar({
|
const _TextSelectionToolbar({
|
||||||
|
this.clipboardStatus,
|
||||||
Key key,
|
Key key,
|
||||||
this.handleCut,
|
this.handleCut,
|
||||||
this.handleCopy,
|
this.handleCopy,
|
||||||
@ -37,6 +39,7 @@ class _TextSelectionToolbar extends StatefulWidget {
|
|||||||
this.isAbove,
|
this.isAbove,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
|
final ClipboardStatusNotifier clipboardStatus;
|
||||||
final VoidCallback handleCut;
|
final VoidCallback handleCut;
|
||||||
final VoidCallback handleCopy;
|
final VoidCallback handleCopy;
|
||||||
final VoidCallback handlePaste;
|
final VoidCallback handlePaste;
|
||||||
@ -50,6 +53,8 @@ class _TextSelectionToolbar extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _TextSelectionToolbarState extends State<_TextSelectionToolbar> with TickerProviderStateMixin {
|
class _TextSelectionToolbarState extends State<_TextSelectionToolbar> with TickerProviderStateMixin {
|
||||||
|
ClipboardStatusNotifier _clipboardStatus;
|
||||||
|
|
||||||
// Whether or not the overflow menu is open. When it is closed, the menu
|
// Whether or not the overflow menu is open. When it is closed, the menu
|
||||||
// items that don't overflow are shown. When it is open, only the overflowing
|
// items that don't overflow are shown. When it is open, only the overflowing
|
||||||
// menu items are shown.
|
// menu items are shown.
|
||||||
@ -66,12 +71,10 @@ class _TextSelectionToolbarState extends State<_TextSelectionToolbar> with Ticke
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
// Close the menu and reset layout calculations, as in when the menu has
|
||||||
void didUpdateWidget(_TextSelectionToolbar oldWidget) {
|
// changed and saved values are no longer relevant. This should be called in
|
||||||
if (((widget.handleCut == null) != (oldWidget.handleCut == null))
|
// setState or another context where a rebuild is happening.
|
||||||
|| ((widget.handleCopy == null) != (oldWidget.handleCopy == null))
|
void _reset() {
|
||||||
|| ((widget.handlePaste == null) != (oldWidget.handlePaste == null))
|
|
||||||
|| ((widget.handleSelectAll == null) != (oldWidget.handleSelectAll == null))) {
|
|
||||||
// Change _TextSelectionToolbarContainer's key when the menu changes in
|
// Change _TextSelectionToolbarContainer's key when the menu changes in
|
||||||
// order to cause it to rebuild. This lets it recalculate its
|
// order to cause it to rebuild. This lets it recalculate its
|
||||||
// saved width for the new set of children, and it prevents AnimatedSize
|
// saved width for the new set of children, and it prevents AnimatedSize
|
||||||
@ -81,18 +84,80 @@ class _TextSelectionToolbarState extends State<_TextSelectionToolbar> with Ticke
|
|||||||
// prevents an empty overflow menu.
|
// prevents an empty overflow menu.
|
||||||
_overflowOpen = false;
|
_overflowOpen = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _onChangedClipboardStatus() {
|
||||||
|
setState(() {
|
||||||
|
// Inform the widget that the value of clipboardStatus has changed.
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_clipboardStatus = widget.clipboardStatus ?? ClipboardStatusNotifier();
|
||||||
|
_clipboardStatus.addListener(_onChangedClipboardStatus);
|
||||||
|
_clipboardStatus.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(_TextSelectionToolbar oldWidget) {
|
||||||
super.didUpdateWidget(oldWidget);
|
super.didUpdateWidget(oldWidget);
|
||||||
|
// If the children are changing, the current page should be reset.
|
||||||
|
if (((widget.handleCut == null) != (oldWidget.handleCut == null))
|
||||||
|
|| ((widget.handleCopy == null) != (oldWidget.handleCopy == null))
|
||||||
|
|| ((widget.handlePaste == null) != (oldWidget.handlePaste == null))
|
||||||
|
|| ((widget.handleSelectAll == null) != (oldWidget.handleSelectAll == null))) {
|
||||||
|
_reset();
|
||||||
|
}
|
||||||
|
if (oldWidget.clipboardStatus == null && widget.clipboardStatus != null) {
|
||||||
|
_clipboardStatus.removeListener(_onChangedClipboardStatus);
|
||||||
|
_clipboardStatus.dispose();
|
||||||
|
_clipboardStatus = widget.clipboardStatus;
|
||||||
|
} else if (oldWidget.clipboardStatus != null) {
|
||||||
|
if (widget.clipboardStatus == null) {
|
||||||
|
_clipboardStatus = ClipboardStatusNotifier();
|
||||||
|
_clipboardStatus.addListener(_onChangedClipboardStatus);
|
||||||
|
oldWidget.clipboardStatus.removeListener(_onChangedClipboardStatus);
|
||||||
|
} else if (widget.clipboardStatus != oldWidget.clipboardStatus) {
|
||||||
|
_clipboardStatus = widget.clipboardStatus;
|
||||||
|
_clipboardStatus.addListener(_onChangedClipboardStatus);
|
||||||
|
oldWidget.clipboardStatus.removeListener(_onChangedClipboardStatus);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (widget.handlePaste != null) {
|
||||||
|
_clipboardStatus.update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
super.dispose();
|
||||||
|
// When used in an Overlay, this can be disposed after its creator has
|
||||||
|
// already disposed _clipboardStatus.
|
||||||
|
if (!_clipboardStatus.disposed) {
|
||||||
|
_clipboardStatus.removeListener(_onChangedClipboardStatus);
|
||||||
|
if (widget.clipboardStatus == null) {
|
||||||
|
_clipboardStatus.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
// Don't render the menu until the state of the clipboard is known.
|
||||||
|
if (widget.handlePaste != null
|
||||||
|
&& _clipboardStatus.value == ClipboardStatus.unknown) {
|
||||||
|
return const SizedBox(width: 0.0, height: 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
|
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
|
||||||
final List<Widget> items = <Widget>[
|
final List<Widget> items = <Widget>[
|
||||||
if (widget.handleCut != null)
|
if (widget.handleCut != null)
|
||||||
_getItem(widget.handleCut, localizations.cutButtonLabel),
|
_getItem(widget.handleCut, localizations.cutButtonLabel),
|
||||||
if (widget.handleCopy != null)
|
if (widget.handleCopy != null)
|
||||||
_getItem(widget.handleCopy, localizations.copyButtonLabel),
|
_getItem(widget.handleCopy, localizations.copyButtonLabel),
|
||||||
if (widget.handlePaste != null)
|
if (widget.handlePaste != null
|
||||||
|
&& _clipboardStatus.value == ClipboardStatus.pasteable)
|
||||||
_getItem(widget.handlePaste, localizations.pasteButtonLabel),
|
_getItem(widget.handlePaste, localizations.pasteButtonLabel),
|
||||||
if (widget.handleSelectAll != null)
|
if (widget.handleSelectAll != null)
|
||||||
_getItem(widget.handleSelectAll, localizations.selectAllButtonLabel),
|
_getItem(widget.handleSelectAll, localizations.selectAllButtonLabel),
|
||||||
@ -103,7 +168,6 @@ class _TextSelectionToolbarState extends State<_TextSelectionToolbar> with Ticke
|
|||||||
return const SizedBox(width: 0.0, height: 0.0);
|
return const SizedBox(width: 0.0, height: 0.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return _TextSelectionToolbarContainer(
|
return _TextSelectionToolbarContainer(
|
||||||
key: _containerKey,
|
key: _containerKey,
|
||||||
overflowOpen: _overflowOpen,
|
overflowOpen: _overflowOpen,
|
||||||
@ -527,6 +591,18 @@ class _TextSelectionToolbarItemsRenderBox extends RenderBox with ContainerRender
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Visit only the children that should be painted.
|
||||||
|
@override
|
||||||
|
void visitChildrenForSemantics(RenderObjectVisitor visitor) {
|
||||||
|
visitChildren((RenderObject renderObjectChild) {
|
||||||
|
final RenderBox child = renderObjectChild as RenderBox;
|
||||||
|
final ToolbarItemsParentData childParentData = child.parentData as ToolbarItemsParentData;
|
||||||
|
if (childParentData.shouldPaint) {
|
||||||
|
visitor(renderObjectChild);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Centers the toolbar around the given anchor, ensuring that it remains on
|
/// Centers the toolbar around the given anchor, ensuring that it remains on
|
||||||
@ -630,6 +706,7 @@ class _MaterialTextSelectionControls extends TextSelectionControls {
|
|||||||
Offset selectionMidpoint,
|
Offset selectionMidpoint,
|
||||||
List<TextSelectionPoint> endpoints,
|
List<TextSelectionPoint> endpoints,
|
||||||
TextSelectionDelegate delegate,
|
TextSelectionDelegate delegate,
|
||||||
|
ClipboardStatusNotifier clipboardStatus,
|
||||||
) {
|
) {
|
||||||
assert(debugCheckHasMediaQuery(context));
|
assert(debugCheckHasMediaQuery(context));
|
||||||
assert(debugCheckHasMaterialLocalizations(context));
|
assert(debugCheckHasMaterialLocalizations(context));
|
||||||
@ -665,8 +742,9 @@ class _MaterialTextSelectionControls extends TextSelectionControls {
|
|||||||
fitsAbove,
|
fitsAbove,
|
||||||
),
|
),
|
||||||
child: _TextSelectionToolbar(
|
child: _TextSelectionToolbar(
|
||||||
|
clipboardStatus: clipboardStatus,
|
||||||
handleCut: canCut(delegate) ? () => handleCut(delegate) : null,
|
handleCut: canCut(delegate) ? () => handleCut(delegate) : null,
|
||||||
handleCopy: canCopy(delegate) ? () => handleCopy(delegate) : null,
|
handleCopy: canCopy(delegate) ? () => handleCopy(delegate, clipboardStatus) : null,
|
||||||
handlePaste: canPaste(delegate) ? () => handlePaste(delegate) : null,
|
handlePaste: canPaste(delegate) ? () => handlePaste(delegate) : null,
|
||||||
handleSelectAll: canSelectAll(delegate) ? () => handleSelectAll(delegate) : null,
|
handleSelectAll: canSelectAll(delegate) ? () => handleSelectAll(delegate) : null,
|
||||||
isAbove: fitsAbove,
|
isAbove: fitsAbove,
|
||||||
|
@ -1144,6 +1144,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||||||
bool _targetCursorVisibility = false;
|
bool _targetCursorVisibility = false;
|
||||||
final ValueNotifier<bool> _cursorVisibilityNotifier = ValueNotifier<bool>(true);
|
final ValueNotifier<bool> _cursorVisibilityNotifier = ValueNotifier<bool>(true);
|
||||||
final GlobalKey _editableKey = GlobalKey();
|
final GlobalKey _editableKey = GlobalKey();
|
||||||
|
final ClipboardStatusNotifier _clipboardStatus = ClipboardStatusNotifier();
|
||||||
|
|
||||||
TextInputConnection _textInputConnection;
|
TextInputConnection _textInputConnection;
|
||||||
TextSelectionOverlay _selectionOverlay;
|
TextSelectionOverlay _selectionOverlay;
|
||||||
@ -1190,11 +1191,18 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||||||
@override
|
@override
|
||||||
bool get selectAllEnabled => widget.toolbarOptions.selectAll;
|
bool get selectAllEnabled => widget.toolbarOptions.selectAll;
|
||||||
|
|
||||||
|
void _onChangedClipboardStatus() {
|
||||||
|
setState(() {
|
||||||
|
// Inform the widget that the value of clipboardStatus has changed.
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// State lifecycle:
|
// State lifecycle:
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
_clipboardStatus.addListener(_onChangedClipboardStatus);
|
||||||
widget.controller.addListener(_didChangeTextEditingValue);
|
widget.controller.addListener(_didChangeTextEditingValue);
|
||||||
_focusAttachment = widget.focusNode.attach(context);
|
_focusAttachment = widget.focusNode.attach(context);
|
||||||
widget.focusNode.addListener(_handleFocusChanged);
|
widget.focusNode.addListener(_handleFocusChanged);
|
||||||
@ -1268,6 +1276,9 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (widget.selectionEnabled && pasteEnabled && widget.selectionControls?.canPaste(this) == true) {
|
||||||
|
_clipboardStatus.update();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -1284,6 +1295,9 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||||||
_selectionOverlay = null;
|
_selectionOverlay = null;
|
||||||
_focusAttachment.detach();
|
_focusAttachment.detach();
|
||||||
widget.focusNode.removeListener(_handleFocusChanged);
|
widget.focusNode.removeListener(_handleFocusChanged);
|
||||||
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
|
_clipboardStatus.removeListener(_onChangedClipboardStatus);
|
||||||
|
_clipboardStatus.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1610,6 +1624,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||||||
|
|
||||||
if (widget.selectionControls != null) {
|
if (widget.selectionControls != null) {
|
||||||
_selectionOverlay = TextSelectionOverlay(
|
_selectionOverlay = TextSelectionOverlay(
|
||||||
|
clipboardStatus: _clipboardStatus,
|
||||||
context: context,
|
context: context,
|
||||||
value: _value,
|
value: _value,
|
||||||
debugRequiredFor: widget,
|
debugRequiredFor: widget,
|
||||||
@ -1980,7 +1995,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||||||
|
|
||||||
VoidCallback _semanticsOnCopy(TextSelectionControls controls) {
|
VoidCallback _semanticsOnCopy(TextSelectionControls controls) {
|
||||||
return widget.selectionEnabled && copyEnabled && _hasFocus && controls?.canCopy(this) == true
|
return widget.selectionEnabled && copyEnabled && _hasFocus && controls?.canCopy(this) == true
|
||||||
? () => controls.handleCopy(this)
|
? () => controls.handleCopy(this, _clipboardStatus)
|
||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1991,7 +2006,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||||||
}
|
}
|
||||||
|
|
||||||
VoidCallback _semanticsOnPaste(TextSelectionControls controls) {
|
VoidCallback _semanticsOnPaste(TextSelectionControls controls) {
|
||||||
return widget.selectionEnabled && pasteEnabled &&_hasFocus && controls?.canPaste(this) == true
|
return widget.selectionEnabled && pasteEnabled && _hasFocus && controls?.canPaste(this) == true && _clipboardStatus.value == ClipboardStatus.pasteable
|
||||||
? () => controls.handlePaste(this)
|
? () => controls.handlePaste(this)
|
||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,7 @@ import 'package:flutter/scheduler.dart';
|
|||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
import 'basic.dart';
|
import 'basic.dart';
|
||||||
|
import 'binding.dart';
|
||||||
import 'constants.dart';
|
import 'constants.dart';
|
||||||
import 'container.dart';
|
import 'container.dart';
|
||||||
import 'editable_text.dart';
|
import 'editable_text.dart';
|
||||||
@ -137,6 +138,7 @@ abstract class TextSelectionControls {
|
|||||||
Offset position,
|
Offset position,
|
||||||
List<TextSelectionPoint> endpoints,
|
List<TextSelectionPoint> endpoints,
|
||||||
TextSelectionDelegate delegate,
|
TextSelectionDelegate delegate,
|
||||||
|
ClipboardStatusNotifier clipboardStatus,
|
||||||
);
|
);
|
||||||
|
|
||||||
/// Returns the size of the selection handle.
|
/// Returns the size of the selection handle.
|
||||||
@ -165,13 +167,16 @@ abstract class TextSelectionControls {
|
|||||||
return delegate.copyEnabled && !delegate.textEditingValue.selection.isCollapsed;
|
return delegate.copyEnabled && !delegate.textEditingValue.selection.isCollapsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether the current [Clipboard] content can be pasted into the text field
|
/// Whether the text field managed by the given `delegate` supports pasting
|
||||||
/// managed by the given `delegate`.
|
/// from the clipboard.
|
||||||
///
|
///
|
||||||
/// Subclasses can use this to decide if they should expose the paste
|
/// Subclasses can use this to decide if they should expose the paste
|
||||||
/// functionality to the user.
|
/// functionality to the user.
|
||||||
|
///
|
||||||
|
/// This does not consider the contents of the clipboard. Subclasses may want
|
||||||
|
/// to, for example, disallow pasting when the clipboard contains an empty
|
||||||
|
/// string.
|
||||||
bool canPaste(TextSelectionDelegate delegate) {
|
bool canPaste(TextSelectionDelegate delegate) {
|
||||||
// TODO(goderbauer): return false when clipboard is empty, https://github.com/flutter/flutter/issues/11254
|
|
||||||
return delegate.pasteEnabled;
|
return delegate.pasteEnabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -213,11 +218,12 @@ abstract class TextSelectionControls {
|
|||||||
///
|
///
|
||||||
/// This is called by subclasses when their copy affordance is activated by
|
/// This is called by subclasses when their copy affordance is activated by
|
||||||
/// the user.
|
/// the user.
|
||||||
void handleCopy(TextSelectionDelegate delegate) {
|
void handleCopy(TextSelectionDelegate delegate, ClipboardStatusNotifier clipboardStatus) {
|
||||||
final TextEditingValue value = delegate.textEditingValue;
|
final TextEditingValue value = delegate.textEditingValue;
|
||||||
Clipboard.setData(ClipboardData(
|
Clipboard.setData(ClipboardData(
|
||||||
text: value.selection.textInside(value.text),
|
text: value.selection.textInside(value.text),
|
||||||
));
|
));
|
||||||
|
clipboardStatus?.update();
|
||||||
delegate.textEditingValue = TextEditingValue(
|
delegate.textEditingValue = TextEditingValue(
|
||||||
text: value.text,
|
text: value.text,
|
||||||
selection: TextSelection.collapsed(offset: value.selection.end),
|
selection: TextSelection.collapsed(offset: value.selection.end),
|
||||||
@ -294,6 +300,7 @@ class TextSelectionOverlay {
|
|||||||
this.selectionDelegate,
|
this.selectionDelegate,
|
||||||
this.dragStartBehavior = DragStartBehavior.start,
|
this.dragStartBehavior = DragStartBehavior.start,
|
||||||
this.onSelectionHandleTapped,
|
this.onSelectionHandleTapped,
|
||||||
|
this.clipboardStatus,
|
||||||
}) : assert(value != null),
|
}) : assert(value != null),
|
||||||
assert(context != null),
|
assert(context != null),
|
||||||
assert(handlesVisible != null),
|
assert(handlesVisible != null),
|
||||||
@ -365,6 +372,13 @@ class TextSelectionOverlay {
|
|||||||
/// {@endtemplate}
|
/// {@endtemplate}
|
||||||
final VoidCallback onSelectionHandleTapped;
|
final VoidCallback onSelectionHandleTapped;
|
||||||
|
|
||||||
|
/// Maintains the status of the clipboard for determining if its contents can
|
||||||
|
/// be pasted or not.
|
||||||
|
///
|
||||||
|
/// Useful because the actual value of the clipboard can only be checked
|
||||||
|
/// asynchronously (see [Clipboard.getData]).
|
||||||
|
final ClipboardStatusNotifier clipboardStatus;
|
||||||
|
|
||||||
/// Controls the fade-in and fade-out animations for the toolbar and handles.
|
/// Controls the fade-in and fade-out animations for the toolbar and handles.
|
||||||
static const Duration fadeDuration = Duration(milliseconds: 150);
|
static const Duration fadeDuration = Duration(milliseconds: 150);
|
||||||
|
|
||||||
@ -576,6 +590,7 @@ class TextSelectionOverlay {
|
|||||||
midpoint,
|
midpoint,
|
||||||
endpoints,
|
endpoints,
|
||||||
selectionDelegate,
|
selectionDelegate,
|
||||||
|
clipboardStatus,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -1486,3 +1501,86 @@ class _TransparentTapGestureRecognizer extends TapGestureRecognizer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A [ValueNotifier] whose [value] indicates whether the current contents of
|
||||||
|
/// the clipboard can be pasted.
|
||||||
|
///
|
||||||
|
/// The contents of the clipboard can only be read asynchronously, via
|
||||||
|
/// [Clipboard.getData], so this maintains a value that can be used
|
||||||
|
/// synchronously. Call [update] to asynchronously update value if needed.
|
||||||
|
class ClipboardStatusNotifier extends ValueNotifier<ClipboardStatus> with WidgetsBindingObserver {
|
||||||
|
/// Create a new ClipboardStatusNotifier.
|
||||||
|
ClipboardStatusNotifier({
|
||||||
|
ClipboardStatus value = ClipboardStatus.unknown,
|
||||||
|
}) : super(value);
|
||||||
|
|
||||||
|
bool _disposed = false;
|
||||||
|
/// True iff this instance has been disposed.
|
||||||
|
bool get disposed => _disposed;
|
||||||
|
|
||||||
|
/// Check the [Clipboard] and update [value] if needed.
|
||||||
|
void update() {
|
||||||
|
Clipboard.getData(Clipboard.kTextPlain).then((ClipboardData data) {
|
||||||
|
final ClipboardStatus clipboardStatus = data != null && data.text != null && data.text.isNotEmpty
|
||||||
|
? ClipboardStatus.pasteable
|
||||||
|
: ClipboardStatus.notPasteable;
|
||||||
|
if (clipboardStatus == value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
value = clipboardStatus;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void addListener(VoidCallback listener) {
|
||||||
|
if (!hasListeners) {
|
||||||
|
WidgetsBinding.instance.addObserver(this);
|
||||||
|
}
|
||||||
|
if (value == ClipboardStatus.unknown) {
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
super.addListener(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void removeListener(VoidCallback listener) {
|
||||||
|
super.removeListener(listener);
|
||||||
|
if (!hasListeners) {
|
||||||
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||||
|
switch (state) {
|
||||||
|
case AppLifecycleState.resumed:
|
||||||
|
update();
|
||||||
|
break;
|
||||||
|
case AppLifecycleState.detached:
|
||||||
|
case AppLifecycleState.inactive:
|
||||||
|
case AppLifecycleState.paused:
|
||||||
|
// Nothing to do.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
super.dispose();
|
||||||
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
|
_disposed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An enumeration of the status of the content on the user's clipboard.
|
||||||
|
enum ClipboardStatus {
|
||||||
|
/// The clipboard content can be pasted, such as a String of nonzero length.
|
||||||
|
pasteable,
|
||||||
|
|
||||||
|
/// The status of the clipboard is unknown. Since getting clipboard data is
|
||||||
|
/// asynchronous (see [Clipboard.getData]), this status often exists while
|
||||||
|
/// waiting to receive the clipboard contents for the first time.
|
||||||
|
unknown,
|
||||||
|
|
||||||
|
/// The content on the clipboard is not pasteable, such as when it is empty.
|
||||||
|
notPasteable,
|
||||||
|
}
|
||||||
|
@ -171,8 +171,11 @@ void main() {
|
|||||||
|
|
||||||
Offset textOffsetToPosition(WidgetTester tester, int offset) => textOffsetToBottomLeftPosition(tester, offset) + const Offset(0, -2);
|
Offset textOffsetToPosition(WidgetTester tester, int offset) => textOffsetToBottomLeftPosition(tester, offset) + const Offset(0, -2);
|
||||||
|
|
||||||
setUp(() {
|
setUp(() async {
|
||||||
EditableText.debugDeterministicCursor = false;
|
EditableText.debugDeterministicCursor = false;
|
||||||
|
// Fill the clipboard so that the PASTE option is available in the text
|
||||||
|
// selection menu.
|
||||||
|
await Clipboard.setData(const ClipboardData(text: 'Clipboard data'));
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets(
|
testWidgets(
|
||||||
@ -1545,7 +1548,7 @@ void main() {
|
|||||||
await tester.tapAt(textOffsetToPosition(tester, index));
|
await tester.tapAt(textOffsetToPosition(tester, index));
|
||||||
await tester.pump(const Duration(milliseconds: 50));
|
await tester.pump(const Duration(milliseconds: 50));
|
||||||
await tester.tapAt(textOffsetToPosition(tester, index));
|
await tester.tapAt(textOffsetToPosition(tester, index));
|
||||||
await tester.pump();
|
await tester.pumpAndSettle();
|
||||||
expect(
|
expect(
|
||||||
controller.selection,
|
controller.selection,
|
||||||
const TextSelection(baseOffset: 0, extentOffset: 7),
|
const TextSelection(baseOffset: 0, extentOffset: 7),
|
||||||
@ -1585,7 +1588,7 @@ void main() {
|
|||||||
const TextSelection.collapsed(offset: 8, affinity: TextAffinity.downstream),
|
const TextSelection.collapsed(offset: 8, affinity: TextAffinity.downstream),
|
||||||
);
|
);
|
||||||
await tester.tapAt(textfieldStart + const Offset(150.0, 5.0));
|
await tester.tapAt(textfieldStart + const Offset(150.0, 5.0));
|
||||||
await tester.pump();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
// Second tap selects the word around the cursor.
|
// Second tap selects the word around the cursor.
|
||||||
expect(
|
expect(
|
||||||
@ -1621,7 +1624,7 @@ void main() {
|
|||||||
final TestGesture gesture =
|
final TestGesture gesture =
|
||||||
await tester.startGesture(textfieldStart + const Offset(150.0, 5.0));
|
await tester.startGesture(textfieldStart + const Offset(150.0, 5.0));
|
||||||
// Hold the press.
|
// Hold the press.
|
||||||
await tester.pump(const Duration(milliseconds: 500));
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
controller.selection,
|
controller.selection,
|
||||||
@ -1760,7 +1763,7 @@ void main() {
|
|||||||
final TestGesture gesture =
|
final TestGesture gesture =
|
||||||
await tester.startGesture(textfieldStart + const Offset(150.0, 5.0));
|
await tester.startGesture(textfieldStart + const Offset(150.0, 5.0));
|
||||||
// Hold the press.
|
// Hold the press.
|
||||||
await tester.pump(const Duration(milliseconds: 500));
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
// The obscured text is treated as one word, should select all
|
// The obscured text is treated as one word, should select all
|
||||||
expect(
|
expect(
|
||||||
@ -1846,7 +1849,7 @@ void main() {
|
|||||||
final Offset textfieldStart = tester.getTopLeft(find.byType(CupertinoTextField));
|
final Offset textfieldStart = tester.getTopLeft(find.byType(CupertinoTextField));
|
||||||
|
|
||||||
await tester.longPressAt(textfieldStart + const Offset(50.0, 5.0));
|
await tester.longPressAt(textfieldStart + const Offset(50.0, 5.0));
|
||||||
await tester.pump();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
// Collapsed cursor for iOS long press.
|
// Collapsed cursor for iOS long press.
|
||||||
expect(
|
expect(
|
||||||
@ -1946,7 +1949,7 @@ void main() {
|
|||||||
expect(find.byType(CupertinoButton), findsNothing);
|
expect(find.byType(CupertinoButton), findsNothing);
|
||||||
|
|
||||||
await gesture.up();
|
await gesture.up();
|
||||||
await tester.pump();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
// The selection isn't affected by the gesture lift.
|
// The selection isn't affected by the gesture lift.
|
||||||
expect(
|
expect(
|
||||||
@ -2021,7 +2024,7 @@ void main() {
|
|||||||
expect(find.byType(CupertinoButton), findsNothing);
|
expect(find.byType(CupertinoButton), findsNothing);
|
||||||
|
|
||||||
await gesture.up();
|
await gesture.up();
|
||||||
await tester.pump();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
// The selection isn't affected by the gesture lift.
|
// The selection isn't affected by the gesture lift.
|
||||||
expect(
|
expect(
|
||||||
@ -2076,7 +2079,7 @@ void main() {
|
|||||||
await tester.pump(const Duration(milliseconds: 500));
|
await tester.pump(const Duration(milliseconds: 500));
|
||||||
|
|
||||||
await tester.longPressAt(textfieldStart + const Offset(100.0, 5.0));
|
await tester.longPressAt(textfieldStart + const Offset(100.0, 5.0));
|
||||||
await tester.pump();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
// Plain collapsed selection at the exact tap position.
|
// Plain collapsed selection at the exact tap position.
|
||||||
expect(
|
expect(
|
||||||
@ -2118,7 +2121,7 @@ void main() {
|
|||||||
const TextSelection.collapsed(offset: 8, affinity: TextAffinity.downstream),
|
const TextSelection.collapsed(offset: 8, affinity: TextAffinity.downstream),
|
||||||
);
|
);
|
||||||
await tester.tapAt(textfieldStart + const Offset(150.0, 5.0));
|
await tester.tapAt(textfieldStart + const Offset(150.0, 5.0));
|
||||||
await tester.pump();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
// Double tap selection.
|
// Double tap selection.
|
||||||
expect(
|
expect(
|
||||||
@ -2155,7 +2158,7 @@ void main() {
|
|||||||
const TextSelection.collapsed(offset: 7, affinity: TextAffinity.upstream),
|
const TextSelection.collapsed(offset: 7, affinity: TextAffinity.upstream),
|
||||||
);
|
);
|
||||||
await tester.tapAt(textfieldStart + const Offset(50.0, 5.0));
|
await tester.tapAt(textfieldStart + const Offset(50.0, 5.0));
|
||||||
await tester.pump(const Duration(milliseconds: 50));
|
await tester.pumpAndSettle();
|
||||||
expect(
|
expect(
|
||||||
controller.selection,
|
controller.selection,
|
||||||
const TextSelection(baseOffset: 0, extentOffset: 7),
|
const TextSelection(baseOffset: 0, extentOffset: 7),
|
||||||
@ -2171,7 +2174,7 @@ void main() {
|
|||||||
const TextSelection.collapsed(offset: 7, affinity: TextAffinity.upstream),
|
const TextSelection.collapsed(offset: 7, affinity: TextAffinity.upstream),
|
||||||
);
|
);
|
||||||
await tester.tapAt(textfieldStart + const Offset(100.0, 5.0));
|
await tester.tapAt(textfieldStart + const Offset(100.0, 5.0));
|
||||||
await tester.pump(const Duration(milliseconds: 50));
|
await tester.pumpAndSettle();
|
||||||
expect(
|
expect(
|
||||||
controller.selection,
|
controller.selection,
|
||||||
const TextSelection(baseOffset: 0, extentOffset: 7),
|
const TextSelection(baseOffset: 0, extentOffset: 7),
|
||||||
@ -2186,7 +2189,7 @@ void main() {
|
|||||||
const TextSelection.collapsed(offset: 8, affinity: TextAffinity.downstream),
|
const TextSelection.collapsed(offset: 8, affinity: TextAffinity.downstream),
|
||||||
);
|
);
|
||||||
await tester.tapAt(textfieldStart + const Offset(150.0, 5.0));
|
await tester.tapAt(textfieldStart + const Offset(150.0, 5.0));
|
||||||
await tester.pump(const Duration(milliseconds: 50));
|
await tester.pumpAndSettle();
|
||||||
expect(
|
expect(
|
||||||
controller.selection,
|
controller.selection,
|
||||||
const TextSelection(baseOffset: 8, extentOffset: 12),
|
const TextSelection(baseOffset: 8, extentOffset: 12),
|
||||||
@ -2230,7 +2233,7 @@ void main() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
await gesture.up();
|
await gesture.up();
|
||||||
await tester.pump();
|
await tester.pumpAndSettle();
|
||||||
// Shows toolbar.
|
// Shows toolbar.
|
||||||
expect(find.byType(CupertinoButton), findsNWidgets(3));
|
expect(find.byType(CupertinoButton), findsNWidgets(3));
|
||||||
});
|
});
|
||||||
@ -3842,7 +3845,7 @@ void main() {
|
|||||||
|
|
||||||
// Long press shows the selection menu.
|
// Long press shows the selection menu.
|
||||||
await tester.longPressAt(textOffsetToPosition(tester, 0));
|
await tester.longPressAt(textOffsetToPosition(tester, 0));
|
||||||
await tester.pump();
|
await tester.pumpAndSettle();
|
||||||
expect(find.text('Paste'), findsOneWidget);
|
expect(find.text('Paste'), findsOneWidget);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -8,9 +8,26 @@ import 'package:flutter/cupertino.dart';
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import '../widgets/text.dart' show textOffsetToPosition;
|
import '../widgets/text.dart' show textOffsetToPosition;
|
||||||
|
|
||||||
|
class MockClipboard {
|
||||||
|
Object _clipboardData = <String, dynamic>{
|
||||||
|
'text': null,
|
||||||
|
};
|
||||||
|
|
||||||
|
Future<dynamic> handleMethodCall(MethodCall methodCall) async {
|
||||||
|
switch (methodCall.method) {
|
||||||
|
case 'Clipboard.getData':
|
||||||
|
return _clipboardData;
|
||||||
|
case 'Clipboard.setData':
|
||||||
|
_clipboardData = methodCall.arguments;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class _LongCupertinoLocalizationsDelegate extends LocalizationsDelegate<CupertinoLocalizations> {
|
class _LongCupertinoLocalizationsDelegate extends LocalizationsDelegate<CupertinoLocalizations> {
|
||||||
const _LongCupertinoLocalizationsDelegate();
|
const _LongCupertinoLocalizationsDelegate();
|
||||||
|
|
||||||
@ -49,6 +66,9 @@ class _LongCupertinoLocalizations extends DefaultCupertinoLocalizations {
|
|||||||
const _LongCupertinoLocalizations longLocalizations = _LongCupertinoLocalizations();
|
const _LongCupertinoLocalizations longLocalizations = _LongCupertinoLocalizations();
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
final MockClipboard mockClipboard = MockClipboard();
|
||||||
|
SystemChannels.platform.setMockMethodCallHandler(mockClipboard.handleMethodCall);
|
||||||
|
|
||||||
// Returns true iff the button is visually enabled.
|
// Returns true iff the button is visually enabled.
|
||||||
bool appearsEnabled(WidgetTester tester, String text) {
|
bool appearsEnabled(WidgetTester tester, String text) {
|
||||||
@ -154,6 +174,55 @@ void main() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('Paste only appears when clipboard has contents', (WidgetTester tester) async {
|
||||||
|
final TextEditingController controller = TextEditingController(
|
||||||
|
text: 'Atwater Peel Sherbrooke Bonaventure',
|
||||||
|
);
|
||||||
|
await tester.pumpWidget(
|
||||||
|
CupertinoApp(
|
||||||
|
home: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
CupertinoTextField(
|
||||||
|
controller: controller,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Make sure the clipboard is empty to start.
|
||||||
|
await Clipboard.setData(const ClipboardData(text: ''));
|
||||||
|
|
||||||
|
// Double tap to selet the first word.
|
||||||
|
const int index = 4;
|
||||||
|
await tester.tapAt(textOffsetToPosition(tester, index));
|
||||||
|
await tester.pump(const Duration(milliseconds: 50));
|
||||||
|
await tester.tapAt(textOffsetToPosition(tester, index));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// No Paste yet, because nothing has been copied.
|
||||||
|
expect(find.text('Paste'), findsNothing);
|
||||||
|
expect(find.text('Copy'), findsOneWidget);
|
||||||
|
expect(find.text('Cut'), findsOneWidget);
|
||||||
|
|
||||||
|
// Tap copy to add something to the clipboard and close the menu.
|
||||||
|
await tester.tapAt(tester.getCenter(find.text('Copy')));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('Copy'), findsNothing);
|
||||||
|
expect(find.text('Cut'), findsNothing);
|
||||||
|
|
||||||
|
// Double tap to show the menu again.
|
||||||
|
await tester.tapAt(textOffsetToPosition(tester, index));
|
||||||
|
await tester.pump(const Duration(milliseconds: 50));
|
||||||
|
await tester.tapAt(textOffsetToPosition(tester, index));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// Paste now shows.
|
||||||
|
expect(find.text('Paste'), findsOneWidget);
|
||||||
|
expect(find.text('Copy'), findsOneWidget);
|
||||||
|
expect(find.text('Cut'), findsOneWidget);
|
||||||
|
}, skip: isBrowser, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }));
|
||||||
|
|
||||||
group('Text selection menu overflow (iOS)', () {
|
group('Text selection menu overflow (iOS)', () {
|
||||||
testWidgets('All menu items show when they fit.', (WidgetTester tester) async {
|
testWidgets('All menu items show when they fit.', (WidgetTester tester) async {
|
||||||
final TextEditingController controller = TextEditingController(text: 'abc def ghi');
|
final TextEditingController controller = TextEditingController(text: 'abc def ghi');
|
||||||
@ -181,7 +250,7 @@ void main() {
|
|||||||
|
|
||||||
// Long press on an empty space to show the selection menu.
|
// Long press on an empty space to show the selection menu.
|
||||||
await tester.longPressAt(textOffsetToPosition(tester, 4));
|
await tester.longPressAt(textOffsetToPosition(tester, 4));
|
||||||
await tester.pump();
|
await tester.pumpAndSettle();
|
||||||
expect(find.text('Cut'), findsNothing);
|
expect(find.text('Cut'), findsNothing);
|
||||||
expect(find.text('Copy'), findsNothing);
|
expect(find.text('Copy'), findsNothing);
|
||||||
expect(find.text('Paste'), findsOneWidget);
|
expect(find.text('Paste'), findsOneWidget);
|
||||||
@ -405,7 +474,7 @@ void main() {
|
|||||||
// Long press on an empty space to show the selection menu, with only the
|
// Long press on an empty space to show the selection menu, with only the
|
||||||
// paste button visible.
|
// paste button visible.
|
||||||
await tester.longPressAt(textOffsetToPosition(tester, 4));
|
await tester.longPressAt(textOffsetToPosition(tester, 4));
|
||||||
await tester.pump();
|
await tester.pumpAndSettle();
|
||||||
expect(find.text(longLocalizations.cutButtonLabel), findsNothing);
|
expect(find.text(longLocalizations.cutButtonLabel), findsNothing);
|
||||||
expect(find.text(longLocalizations.copyButtonLabel), findsNothing);
|
expect(find.text(longLocalizations.copyButtonLabel), findsNothing);
|
||||||
expect(find.text(longLocalizations.pasteButtonLabel), findsOneWidget);
|
expect(find.text(longLocalizations.pasteButtonLabel), findsOneWidget);
|
||||||
|
@ -3,12 +3,31 @@
|
|||||||
// found in the LICENSE file.
|
// found in the LICENSE file.
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
import '../rendering/mock_canvas.dart';
|
import '../rendering/mock_canvas.dart';
|
||||||
import 'feedback_tester.dart';
|
import 'feedback_tester.dart';
|
||||||
|
|
||||||
|
class MockClipboard {
|
||||||
|
Object _clipboardData = <String, dynamic>{
|
||||||
|
'text': null,
|
||||||
|
};
|
||||||
|
|
||||||
|
Future<dynamic> handleMethodCall(MethodCall methodCall) async {
|
||||||
|
switch (methodCall.method) {
|
||||||
|
case 'Clipboard.getData':
|
||||||
|
return _clipboardData;
|
||||||
|
case 'Clipboard.setData':
|
||||||
|
_clipboardData = methodCall.arguments;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
final MockClipboard mockClipboard = MockClipboard();
|
||||||
|
|
||||||
DateTime firstDate;
|
DateTime firstDate;
|
||||||
DateTime lastDate;
|
DateTime lastDate;
|
||||||
@ -35,7 +54,7 @@ void main() {
|
|||||||
return tester.widget<TextField>(find.byType(TextField));
|
return tester.widget<TextField>(find.byType(TextField));
|
||||||
}
|
}
|
||||||
|
|
||||||
setUp(() {
|
setUp(() async {
|
||||||
firstDate = DateTime(2001, DateTime.january, 1);
|
firstDate = DateTime(2001, DateTime.january, 1);
|
||||||
lastDate = DateTime(2031, DateTime.december, 31);
|
lastDate = DateTime(2031, DateTime.december, 31);
|
||||||
initialDate = DateTime(2016, DateTime.january, 15);
|
initialDate = DateTime(2016, DateTime.january, 15);
|
||||||
@ -51,6 +70,15 @@ void main() {
|
|||||||
fieldHintText = null;
|
fieldHintText = null;
|
||||||
fieldLabelText = null;
|
fieldLabelText = null;
|
||||||
helpText = null;
|
helpText = null;
|
||||||
|
|
||||||
|
// Fill the clipboard so that the PASTE option is available in the text
|
||||||
|
// selection menu.
|
||||||
|
SystemChannels.platform.setMockMethodCallHandler(mockClipboard.handleMethodCall);
|
||||||
|
await Clipboard.setData(const ClipboardData(text: 'Clipboard data'));
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDown(() {
|
||||||
|
SystemChannels.platform.setMockMethodCallHandler(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
Future<void> prepareDatePicker(WidgetTester tester, Future<void> callback(Future<DateTime> date)) async {
|
Future<void> prepareDatePicker(WidgetTester tester, Future<void> callback(Future<DateTime> date)) async {
|
||||||
@ -1018,7 +1046,6 @@ void main() {
|
|||||||
|
|
||||||
semantics.dispose();
|
semantics.dispose();
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
group('Screen configurations', () {
|
group('Screen configurations', () {
|
||||||
|
@ -9,7 +9,37 @@ import 'package:flutter_test/flutter_test.dart';
|
|||||||
|
|
||||||
import '../widgets/semantics_tester.dart';
|
import '../widgets/semantics_tester.dart';
|
||||||
|
|
||||||
|
class MockClipboard {
|
||||||
|
Object _clipboardData = <String, dynamic>{
|
||||||
|
'text': null,
|
||||||
|
};
|
||||||
|
|
||||||
|
Future<dynamic> handleMethodCall(MethodCall methodCall) async {
|
||||||
|
switch (methodCall.method) {
|
||||||
|
case 'Clipboard.getData':
|
||||||
|
return _clipboardData;
|
||||||
|
case 'Clipboard.setData':
|
||||||
|
_clipboardData = methodCall.arguments;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
final MockClipboard mockClipboard = MockClipboard();
|
||||||
|
|
||||||
|
setUp(() async {
|
||||||
|
// Fill the clipboard so that the PASTE option is available in the text
|
||||||
|
// selection menu.
|
||||||
|
SystemChannels.platform.setMockMethodCallHandler(mockClipboard.handleMethodCall);
|
||||||
|
await Clipboard.setData(const ClipboardData(text: 'Clipboard data'));
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDown(() {
|
||||||
|
SystemChannels.platform.setMockMethodCallHandler(null);
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('Can open and close search', (WidgetTester tester) async {
|
testWidgets('Can open and close search', (WidgetTester tester) async {
|
||||||
final _TestSearchDelegate delegate = _TestSearchDelegate();
|
final _TestSearchDelegate delegate = _TestSearchDelegate();
|
||||||
final List<String> selectedResults = <String>[];
|
final List<String> selectedResults = <String>[];
|
||||||
|
@ -142,8 +142,11 @@ void main() {
|
|||||||
kThreeLines +
|
kThreeLines +
|
||||||
"\nFourth line won't display and ends at";
|
"\nFourth line won't display and ends at";
|
||||||
|
|
||||||
setUp(() {
|
setUp(() async {
|
||||||
debugResetSemanticsIdCounter();
|
debugResetSemanticsIdCounter();
|
||||||
|
// Fill the clipboard so that the PASTE option is available in the text
|
||||||
|
// selection menu.
|
||||||
|
await Clipboard.setData(const ClipboardData(text: 'Clipboard data'));
|
||||||
});
|
});
|
||||||
|
|
||||||
final Key textFieldKey = UniqueKey();
|
final Key textFieldKey = UniqueKey();
|
||||||
@ -1045,7 +1048,7 @@ void main() {
|
|||||||
const int dIndex = 3;
|
const int dIndex = 3;
|
||||||
final Offset dPos = textOffsetToPosition(tester, dIndex);
|
final Offset dPos = textOffsetToPosition(tester, dIndex);
|
||||||
await tester.longPressAt(dPos);
|
await tester.longPressAt(dPos);
|
||||||
await tester.pump();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
// Context menu should not have paste and cut.
|
// Context menu should not have paste and cut.
|
||||||
expect(find.text('COPY'), findsOneWidget);
|
expect(find.text('COPY'), findsOneWidget);
|
||||||
@ -1783,6 +1786,8 @@ void main() {
|
|||||||
renderEditable,
|
renderEditable,
|
||||||
);
|
);
|
||||||
await tester.tapAt(endpoints[0].point + const Offset(1.0, 1.0));
|
await tester.tapAt(endpoints[0].point + const Offset(1.0, 1.0));
|
||||||
|
// Pump an extra frame to allow the selection menu to read the clipboard.
|
||||||
|
await tester.pump();
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
|
|
||||||
// Toolbar should fade in. Starting at 0% opacity.
|
// Toolbar should fade in. Starting at 0% opacity.
|
||||||
@ -1893,7 +1898,7 @@ void main() {
|
|||||||
// Long press to select text.
|
// Long press to select text.
|
||||||
final Offset bPos = textOffsetToPosition(tester, 1);
|
final Offset bPos = textOffsetToPosition(tester, 1);
|
||||||
await tester.longPressAt(bPos, pointer: 7);
|
await tester.longPressAt(bPos, pointer: 7);
|
||||||
await tester.pump();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
// Should only have paste option when whole obscure text is selected.
|
// Should only have paste option when whole obscure text is selected.
|
||||||
expect(find.text('PASTE'), findsOneWidget);
|
expect(find.text('PASTE'), findsOneWidget);
|
||||||
@ -1905,7 +1910,7 @@ void main() {
|
|||||||
final Offset iPos = textOffsetToPosition(tester, 10);
|
final Offset iPos = textOffsetToPosition(tester, 10);
|
||||||
final Offset slightRight = iPos + const Offset(30.0, 0.0);
|
final Offset slightRight = iPos + const Offset(30.0, 0.0);
|
||||||
await tester.longPressAt(slightRight, pointer: 7);
|
await tester.longPressAt(slightRight, pointer: 7);
|
||||||
await tester.pump();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
// Should have paste and select all options when collapse.
|
// Should have paste and select all options when collapse.
|
||||||
expect(find.text('PASTE'), findsOneWidget);
|
expect(find.text('PASTE'), findsOneWidget);
|
||||||
@ -5037,6 +5042,78 @@ void main() {
|
|||||||
semantics.dispose();
|
semantics.dispose();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('When clipboard empty, no semantics paste option', (WidgetTester tester) async {
|
||||||
|
const String textInTextField = 'Hello';
|
||||||
|
|
||||||
|
final SemanticsTester semantics = SemanticsTester(tester);
|
||||||
|
final SemanticsOwner semanticsOwner = tester.binding.pipelineOwner.semanticsOwner;
|
||||||
|
final TextEditingController controller = TextEditingController()
|
||||||
|
..text = textInTextField;
|
||||||
|
final Key key = UniqueKey();
|
||||||
|
|
||||||
|
// Clear the clipboard.
|
||||||
|
await Clipboard.setData(const ClipboardData(text: ''));
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
overlay(
|
||||||
|
child: TextField(
|
||||||
|
key: key,
|
||||||
|
controller: controller,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const int inputFieldId = 1;
|
||||||
|
|
||||||
|
expect(semantics, hasSemantics(
|
||||||
|
TestSemantics.root(
|
||||||
|
children: <TestSemantics>[
|
||||||
|
TestSemantics(
|
||||||
|
id: inputFieldId,
|
||||||
|
flags: <SemanticsFlag>[SemanticsFlag.isTextField],
|
||||||
|
actions: <SemanticsAction>[SemanticsAction.tap],
|
||||||
|
value: textInTextField,
|
||||||
|
textDirection: TextDirection.ltr,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
ignoreRect: true, ignoreTransform: true,
|
||||||
|
));
|
||||||
|
|
||||||
|
semanticsOwner.performAction(inputFieldId, SemanticsAction.tap);
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(semantics, hasSemantics(
|
||||||
|
TestSemantics.root(
|
||||||
|
children: <TestSemantics>[
|
||||||
|
TestSemantics(
|
||||||
|
id: inputFieldId,
|
||||||
|
flags: <SemanticsFlag>[
|
||||||
|
SemanticsFlag.isTextField,
|
||||||
|
SemanticsFlag.isFocused,
|
||||||
|
],
|
||||||
|
actions: <SemanticsAction>[
|
||||||
|
SemanticsAction.tap,
|
||||||
|
SemanticsAction.moveCursorBackwardByCharacter,
|
||||||
|
SemanticsAction.moveCursorBackwardByWord,
|
||||||
|
SemanticsAction.setSelection,
|
||||||
|
// No paste option.
|
||||||
|
],
|
||||||
|
value: textInTextField,
|
||||||
|
textDirection: TextDirection.ltr,
|
||||||
|
textSelection: const TextSelection(
|
||||||
|
baseOffset: textInTextField.length,
|
||||||
|
extentOffset: textInTextField.length,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
ignoreRect: true, ignoreTransform: true,
|
||||||
|
));
|
||||||
|
|
||||||
|
semantics.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('TextField throws when not descended from a Material widget', (WidgetTester tester) async {
|
testWidgets('TextField throws when not descended from a Material widget', (WidgetTester tester) async {
|
||||||
const Widget textField = TextField();
|
const Widget textField = TextField();
|
||||||
await tester.pumpWidget(textField);
|
await tester.pumpWidget(textField);
|
||||||
@ -5798,7 +5875,7 @@ void main() {
|
|||||||
const TextSelection.collapsed(offset: 8, affinity: TextAffinity.downstream),
|
const TextSelection.collapsed(offset: 8, affinity: TextAffinity.downstream),
|
||||||
);
|
);
|
||||||
await tester.tapAt(textfieldStart + const Offset(150.0, 9.0));
|
await tester.tapAt(textfieldStart + const Offset(150.0, 9.0));
|
||||||
await tester.pump();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
// Second tap selects the word around the cursor.
|
// Second tap selects the word around the cursor.
|
||||||
expect(
|
expect(
|
||||||
@ -5843,7 +5920,7 @@ void main() {
|
|||||||
const TextSelection.collapsed(offset: 9),
|
const TextSelection.collapsed(offset: 9),
|
||||||
);
|
);
|
||||||
await tester.tapAt(textfieldStart + const Offset(150.0, 9.0));
|
await tester.tapAt(textfieldStart + const Offset(150.0, 9.0));
|
||||||
await tester.pump();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
// Second tap selects the word around the cursor.
|
// Second tap selects the word around the cursor.
|
||||||
expect(
|
expect(
|
||||||
@ -5894,7 +5971,7 @@ void main() {
|
|||||||
|
|
||||||
// Second tap selects the word around the cursor.
|
// Second tap selects the word around the cursor.
|
||||||
await tester.tapAt(textOffsetToPosition(tester, index));
|
await tester.tapAt(textOffsetToPosition(tester, index));
|
||||||
await tester.pump();
|
await tester.pumpAndSettle();
|
||||||
expect(
|
expect(
|
||||||
controller.selection,
|
controller.selection,
|
||||||
const TextSelection(baseOffset: 0, extentOffset: 7),
|
const TextSelection(baseOffset: 0, extentOffset: 7),
|
||||||
@ -5926,14 +6003,14 @@ void main() {
|
|||||||
await tester.tapAt(textOffsetToPosition(tester, 0));
|
await tester.tapAt(textOffsetToPosition(tester, 0));
|
||||||
await tester.pump(const Duration(milliseconds: 50));
|
await tester.pump(const Duration(milliseconds: 50));
|
||||||
await tester.tapAt(textOffsetToPosition(tester, 0));
|
await tester.tapAt(textOffsetToPosition(tester, 0));
|
||||||
await tester.pump();
|
await tester.pumpAndSettle();
|
||||||
expect(find.text('PASTE'), findsOneWidget);
|
expect(find.text('PASTE'), findsOneWidget);
|
||||||
|
|
||||||
// Double tap again keeps the selection menu visible.
|
// Double tap again keeps the selection menu visible.
|
||||||
await tester.tapAt(textOffsetToPosition(tester, 0));
|
await tester.tapAt(textOffsetToPosition(tester, 0));
|
||||||
await tester.pump(const Duration(milliseconds: 50));
|
await tester.pump(const Duration(milliseconds: 50));
|
||||||
await tester.tapAt(textOffsetToPosition(tester, 0));
|
await tester.tapAt(textOffsetToPosition(tester, 0));
|
||||||
await tester.pump();
|
await tester.pumpAndSettle();
|
||||||
expect(find.text('PASTE'), findsOneWidget);
|
expect(find.text('PASTE'), findsOneWidget);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -5958,7 +6035,7 @@ void main() {
|
|||||||
|
|
||||||
// Long press shows the selection menu.
|
// Long press shows the selection menu.
|
||||||
await tester.longPressAt(textOffsetToPosition(tester, 0));
|
await tester.longPressAt(textOffsetToPosition(tester, 0));
|
||||||
await tester.pump();
|
await tester.pumpAndSettle();
|
||||||
expect(find.text('PASTE'), findsOneWidget);
|
expect(find.text('PASTE'), findsOneWidget);
|
||||||
|
|
||||||
// Long press again keeps the selection menu visible.
|
// Long press again keeps the selection menu visible.
|
||||||
@ -5988,7 +6065,7 @@ void main() {
|
|||||||
|
|
||||||
// Long press shows the selection menu.
|
// Long press shows the selection menu.
|
||||||
await tester.longPress(find.byType(TextField));
|
await tester.longPress(find.byType(TextField));
|
||||||
await tester.pump();
|
await tester.pumpAndSettle();
|
||||||
expect(find.text('PASTE'), findsOneWidget);
|
expect(find.text('PASTE'), findsOneWidget);
|
||||||
|
|
||||||
// Tap hides the selection menu.
|
// Tap hides the selection menu.
|
||||||
@ -6023,7 +6100,7 @@ void main() {
|
|||||||
// Long press shows the selection menu.
|
// Long press shows the selection menu.
|
||||||
expect(find.text('PASTE'), findsNothing);
|
expect(find.text('PASTE'), findsNothing);
|
||||||
await tester.longPress(find.byType(TextField));
|
await tester.longPress(find.byType(TextField));
|
||||||
await tester.pump();
|
await tester.pumpAndSettle();
|
||||||
expect(find.text('PASTE'), findsOneWidget);
|
expect(find.text('PASTE'), findsOneWidget);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -6053,7 +6130,7 @@ void main() {
|
|||||||
final TestGesture gesture =
|
final TestGesture gesture =
|
||||||
await tester.startGesture(textfieldStart + const Offset(150.0, 9.0));
|
await tester.startGesture(textfieldStart + const Offset(150.0, 9.0));
|
||||||
// Hold the press.
|
// Hold the press.
|
||||||
await tester.pump(const Duration(milliseconds: 500));
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
controller.selection,
|
controller.selection,
|
||||||
@ -6142,7 +6219,7 @@ void main() {
|
|||||||
final Offset textfieldStart = tester.getTopLeft(find.byType(TextField));
|
final Offset textfieldStart = tester.getTopLeft(find.byType(TextField));
|
||||||
|
|
||||||
await tester.longPressAt(textfieldStart + const Offset(50.0, 9.0));
|
await tester.longPressAt(textfieldStart + const Offset(50.0, 9.0));
|
||||||
await tester.pump();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
// Collapsed cursor for iOS long press.
|
// Collapsed cursor for iOS long press.
|
||||||
expect(
|
expect(
|
||||||
@ -6175,7 +6252,7 @@ void main() {
|
|||||||
final Offset textfieldStart = tester.getTopLeft(find.byType(TextField));
|
final Offset textfieldStart = tester.getTopLeft(find.byType(TextField));
|
||||||
|
|
||||||
await tester.longPressAt(textfieldStart + const Offset(50.0, 9.0));
|
await tester.longPressAt(textfieldStart + const Offset(50.0, 9.0));
|
||||||
await tester.pump();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
controller.selection,
|
controller.selection,
|
||||||
@ -6278,7 +6355,7 @@ void main() {
|
|||||||
expect(find.byType(CupertinoButton), findsNothing);
|
expect(find.byType(CupertinoButton), findsNothing);
|
||||||
|
|
||||||
await gesture.up();
|
await gesture.up();
|
||||||
await tester.pump();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
// The selection isn't affected by the gesture lift.
|
// The selection isn't affected by the gesture lift.
|
||||||
expect(
|
expect(
|
||||||
@ -6352,7 +6429,7 @@ void main() {
|
|||||||
expect(find.byType(CupertinoButton), findsNothing);
|
expect(find.byType(CupertinoButton), findsNothing);
|
||||||
|
|
||||||
await gesture.up();
|
await gesture.up();
|
||||||
await tester.pump();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
// The selection isn't affected by the gesture lift.
|
// The selection isn't affected by the gesture lift.
|
||||||
expect(
|
expect(
|
||||||
@ -6409,7 +6486,7 @@ void main() {
|
|||||||
await tester.pump(const Duration(milliseconds: 500));
|
await tester.pump(const Duration(milliseconds: 500));
|
||||||
|
|
||||||
await tester.longPressAt(textfieldStart + const Offset(100.0, 9.0));
|
await tester.longPressAt(textfieldStart + const Offset(100.0, 9.0));
|
||||||
await tester.pump();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
// Plain collapsed selection at the exact tap position.
|
// Plain collapsed selection at the exact tap position.
|
||||||
expect(
|
expect(
|
||||||
@ -6452,7 +6529,7 @@ void main() {
|
|||||||
const TextSelection.collapsed(offset: 8, affinity: TextAffinity.downstream),
|
const TextSelection.collapsed(offset: 8, affinity: TextAffinity.downstream),
|
||||||
);
|
);
|
||||||
await tester.tapAt(textfieldStart + const Offset(150.0, 9.0));
|
await tester.tapAt(textfieldStart + const Offset(150.0, 9.0));
|
||||||
await tester.pump();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
// Double tap selection.
|
// Double tap selection.
|
||||||
expect(
|
expect(
|
||||||
@ -6487,7 +6564,7 @@ void main() {
|
|||||||
const TextSelection.collapsed(offset: 7, affinity: TextAffinity.upstream),
|
const TextSelection.collapsed(offset: 7, affinity: TextAffinity.upstream),
|
||||||
);
|
);
|
||||||
await tester.tapAt(textfieldStart + const Offset(50.0, 9.0));
|
await tester.tapAt(textfieldStart + const Offset(50.0, 9.0));
|
||||||
await tester.pump(const Duration(milliseconds: 50));
|
await tester.pumpAndSettle();
|
||||||
expect(
|
expect(
|
||||||
controller.selection,
|
controller.selection,
|
||||||
const TextSelection(baseOffset: 0, extentOffset: 7),
|
const TextSelection(baseOffset: 0, extentOffset: 7),
|
||||||
@ -6503,7 +6580,7 @@ void main() {
|
|||||||
const TextSelection.collapsed(offset: 7, affinity: TextAffinity.upstream),
|
const TextSelection.collapsed(offset: 7, affinity: TextAffinity.upstream),
|
||||||
);
|
);
|
||||||
await tester.tapAt(textfieldStart + const Offset(100.0, 9.0));
|
await tester.tapAt(textfieldStart + const Offset(100.0, 9.0));
|
||||||
await tester.pump(const Duration(milliseconds: 50));
|
await tester.pumpAndSettle();
|
||||||
expect(
|
expect(
|
||||||
controller.selection,
|
controller.selection,
|
||||||
const TextSelection(baseOffset: 0, extentOffset: 7),
|
const TextSelection(baseOffset: 0, extentOffset: 7),
|
||||||
@ -6518,7 +6595,7 @@ void main() {
|
|||||||
const TextSelection.collapsed(offset: 8, affinity: TextAffinity.downstream),
|
const TextSelection.collapsed(offset: 8, affinity: TextAffinity.downstream),
|
||||||
);
|
);
|
||||||
await tester.tapAt(textfieldStart + const Offset(150.0, 9.0));
|
await tester.tapAt(textfieldStart + const Offset(150.0, 9.0));
|
||||||
await tester.pump(const Duration(milliseconds: 50));
|
await tester.pumpAndSettle();
|
||||||
expect(
|
expect(
|
||||||
controller.selection,
|
controller.selection,
|
||||||
const TextSelection(baseOffset: 8, extentOffset: 12),
|
const TextSelection(baseOffset: 8, extentOffset: 12),
|
||||||
@ -6610,7 +6687,7 @@ void main() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
await gesture.up();
|
await gesture.up();
|
||||||
await tester.pump();
|
await tester.pumpAndSettle();
|
||||||
expect(find.byType(CupertinoButton), findsNWidgets(3));
|
expect(find.byType(CupertinoButton), findsNWidgets(3));
|
||||||
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
|
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
|
||||||
|
|
||||||
|
@ -5,10 +5,35 @@
|
|||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import '../widgets/text.dart' show findRenderEditable, globalize, textOffsetToPosition;
|
import '../widgets/text.dart' show findRenderEditable, globalize, textOffsetToPosition;
|
||||||
|
|
||||||
|
class MockClipboard {
|
||||||
|
Object _clipboardData = <String, dynamic>{
|
||||||
|
'text': null,
|
||||||
|
};
|
||||||
|
|
||||||
|
Future<dynamic> handleMethodCall(MethodCall methodCall) async {
|
||||||
|
switch (methodCall.method) {
|
||||||
|
case 'Clipboard.getData':
|
||||||
|
return _clipboardData;
|
||||||
|
case 'Clipboard.setData':
|
||||||
|
_clipboardData = methodCall.arguments;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
final MockClipboard mockClipboard = MockClipboard();
|
||||||
|
SystemChannels.platform.setMockMethodCallHandler(mockClipboard.handleMethodCall);
|
||||||
|
|
||||||
|
setUp(() async {
|
||||||
|
await Clipboard.setData(const ClipboardData(text: 'clipboard data'));
|
||||||
|
});
|
||||||
|
|
||||||
group('canSelectAll', () {
|
group('canSelectAll', () {
|
||||||
Widget createEditableText({
|
Widget createEditableText({
|
||||||
Key key,
|
Key key,
|
||||||
@ -104,7 +129,7 @@ void main() {
|
|||||||
expect(endpoints.length, 1);
|
expect(endpoints.length, 1);
|
||||||
final Offset handlePos = endpoints[0].point + const Offset(0.0, 1.0);
|
final Offset handlePos = endpoints[0].point + const Offset(0.0, 1.0);
|
||||||
await tester.tapAt(handlePos, pointer: 7);
|
await tester.tapAt(handlePos, pointer: 7);
|
||||||
await tester.pump();
|
await tester.pumpAndSettle();
|
||||||
expect(find.text('CUT'), findsNothing);
|
expect(find.text('CUT'), findsNothing);
|
||||||
expect(find.text('COPY'), findsNothing);
|
expect(find.text('COPY'), findsNothing);
|
||||||
expect(find.text('PASTE'), findsOneWidget);
|
expect(find.text('PASTE'), findsOneWidget);
|
||||||
@ -235,7 +260,7 @@ void main() {
|
|||||||
// Long press to show the menu.
|
// Long press to show the menu.
|
||||||
final Offset textOffset = textOffsetToPosition(tester, 1);
|
final Offset textOffset = textOffsetToPosition(tester, 1);
|
||||||
await tester.longPressAt(textOffset);
|
await tester.longPressAt(textOffset);
|
||||||
await tester.pump();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
// The last two buttons are missing, and a more button is shown.
|
// The last two buttons are missing, and a more button is shown.
|
||||||
expect(find.text('CUT'), findsOneWidget);
|
expect(find.text('CUT'), findsOneWidget);
|
||||||
@ -301,7 +326,7 @@ void main() {
|
|||||||
// Long press to show the menu.
|
// Long press to show the menu.
|
||||||
final Offset textOffset = textOffsetToPosition(tester, 1);
|
final Offset textOffset = textOffsetToPosition(tester, 1);
|
||||||
await tester.longPressAt(textOffset);
|
await tester.longPressAt(textOffset);
|
||||||
await tester.pump();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
// The last button is missing, and a more button is shown.
|
// The last button is missing, and a more button is shown.
|
||||||
expect(find.text('CUT'), findsOneWidget);
|
expect(find.text('CUT'), findsOneWidget);
|
||||||
@ -413,7 +438,7 @@ void main() {
|
|||||||
|
|
||||||
// Long press to show the menu.
|
// Long press to show the menu.
|
||||||
await tester.longPressAt(textOffsetToPosition(tester, 1));
|
await tester.longPressAt(textOffsetToPosition(tester, 1));
|
||||||
await tester.pump();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
// The last button is missing, and a more button is shown.
|
// The last button is missing, and a more button is shown.
|
||||||
expect(find.text('CUT'), findsOneWidget);
|
expect(find.text('CUT'), findsOneWidget);
|
||||||
@ -488,7 +513,7 @@ void main() {
|
|||||||
expect(endpoints.length, 1);
|
expect(endpoints.length, 1);
|
||||||
final Offset handlePos = endpoints[0].point + const Offset(0.0, 1.0);
|
final Offset handlePos = endpoints[0].point + const Offset(0.0, 1.0);
|
||||||
await tester.tapAt(handlePos, pointer: 7);
|
await tester.tapAt(handlePos, pointer: 7);
|
||||||
await tester.pump();
|
await tester.pumpAndSettle();
|
||||||
expect(find.text('CUT'), findsNothing);
|
expect(find.text('CUT'), findsNothing);
|
||||||
expect(find.text('COPY'), findsNothing);
|
expect(find.text('COPY'), findsNothing);
|
||||||
expect(find.text('PASTE'), findsOneWidget);
|
expect(find.text('PASTE'), findsOneWidget);
|
||||||
@ -556,4 +581,58 @@ void main() {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('Paste only appears when clipboard has contents', (WidgetTester tester) async {
|
||||||
|
final TextEditingController controller = TextEditingController(
|
||||||
|
text: 'Atwater Peel Sherbrooke Bonaventure',
|
||||||
|
);
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: Material(
|
||||||
|
child: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
TextField(
|
||||||
|
controller: controller,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Make sure the clipboard is empty to start.
|
||||||
|
await Clipboard.setData(const ClipboardData(text: ''));
|
||||||
|
|
||||||
|
// Double tap to selet the first word.
|
||||||
|
const int index = 4;
|
||||||
|
await tester.tapAt(textOffsetToPosition(tester, index));
|
||||||
|
await tester.pump(const Duration(milliseconds: 50));
|
||||||
|
await tester.tapAt(textOffsetToPosition(tester, index));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// No Paste yet, because nothing has been copied.
|
||||||
|
expect(find.text('PASTE'), findsNothing);
|
||||||
|
expect(find.text('COPY'), findsOneWidget);
|
||||||
|
expect(find.text('CUT'), findsOneWidget);
|
||||||
|
expect(find.text('SELECT ALL'), findsOneWidget);
|
||||||
|
|
||||||
|
// Tap copy to add something to the clipboard and close the menu.
|
||||||
|
await tester.tapAt(tester.getCenter(find.text('COPY')));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('COPY'), findsNothing);
|
||||||
|
expect(find.text('CUT'), findsNothing);
|
||||||
|
expect(find.text('SELECT ALL'), findsNothing);
|
||||||
|
|
||||||
|
// Double tap to show the menu again.
|
||||||
|
await tester.tapAt(textOffsetToPosition(tester, index));
|
||||||
|
await tester.pump(const Duration(milliseconds: 50));
|
||||||
|
await tester.tapAt(textOffsetToPosition(tester, index));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// Paste now shows.
|
||||||
|
expect(find.text('COPY'), findsOneWidget);
|
||||||
|
expect(find.text('CUT'), findsOneWidget);
|
||||||
|
expect(find.text('PASTE'), findsOneWidget);
|
||||||
|
expect(find.text('SELECT ALL'), findsOneWidget);
|
||||||
|
}, skip: isBrowser);
|
||||||
}
|
}
|
||||||
|
@ -19,6 +19,12 @@ const TextStyle textStyle = TextStyle();
|
|||||||
const Color cursorColor = Color.fromARGB(0xFF, 0xFF, 0x00, 0x00);
|
const Color cursorColor = Color.fromARGB(0xFF, 0xFF, 0x00, 0x00);
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
|
setUp(() async {
|
||||||
|
// Fill the clipboard so that the PASTE option is available in the text
|
||||||
|
// selection menu.
|
||||||
|
await Clipboard.setData(const ClipboardData(text: 'Clipboard data'));
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('cursor has expected width and radius', (WidgetTester tester) async {
|
testWidgets('cursor has expected width and radius', (WidgetTester tester) async {
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
MediaQuery(data: const MediaQueryData(devicePixelRatio: 1.0),
|
MediaQuery(data: const MediaQueryData(devicePixelRatio: 1.0),
|
||||||
@ -39,7 +45,6 @@ void main() {
|
|||||||
expect(editableText.cursorRadius.x, 2.0);
|
expect(editableText.cursorRadius.x, 2.0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
testWidgets('cursor layout has correct width', (WidgetTester tester) async {
|
testWidgets('cursor layout has correct width', (WidgetTester tester) async {
|
||||||
final GlobalKey<EditableTextState> editableTextKey = GlobalKey<EditableTextState>();
|
final GlobalKey<EditableTextState> editableTextKey = GlobalKey<EditableTextState>();
|
||||||
|
|
||||||
@ -132,7 +137,7 @@ void main() {
|
|||||||
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();
|
tester.state<EditableTextState>(textFinder).showToolbar();
|
||||||
await tester.pump();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
await tester.tap(find.text('PASTE'));
|
await tester.tap(find.text('PASTE'));
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
|
@ -48,9 +48,12 @@ void main() {
|
|||||||
final MockClipboard mockClipboard = MockClipboard();
|
final MockClipboard mockClipboard = MockClipboard();
|
||||||
SystemChannels.platform.setMockMethodCallHandler(mockClipboard.handleMethodCall);
|
SystemChannels.platform.setMockMethodCallHandler(mockClipboard.handleMethodCall);
|
||||||
|
|
||||||
setUp(() {
|
setUp(() async {
|
||||||
debugResetSemanticsIdCounter();
|
debugResetSemanticsIdCounter();
|
||||||
controller = TextEditingController();
|
controller = TextEditingController();
|
||||||
|
// Fill the clipboard so that the PASTE option is available in the text
|
||||||
|
// selection menu.
|
||||||
|
await Clipboard.setData(const ClipboardData(text: 'Clipboard data'));
|
||||||
});
|
});
|
||||||
|
|
||||||
tearDown(() {
|
tearDown(() {
|
||||||
@ -961,7 +964,7 @@ void main() {
|
|||||||
|
|
||||||
// Can't show the toolbar when there's no focus.
|
// Can't show the toolbar when there's no focus.
|
||||||
expect(state.showToolbar(), false);
|
expect(state.showToolbar(), false);
|
||||||
await tester.pump();
|
await tester.pumpAndSettle();
|
||||||
expect(find.text('PASTE'), findsNothing);
|
expect(find.text('PASTE'), findsNothing);
|
||||||
|
|
||||||
// Can show the toolbar when focused even though there's no text.
|
// Can show the toolbar when focused even though there's no text.
|
||||||
@ -971,7 +974,7 @@ void main() {
|
|||||||
);
|
);
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
expect(state.showToolbar(), true);
|
expect(state.showToolbar(), true);
|
||||||
await tester.pump();
|
await tester.pumpAndSettle();
|
||||||
expect(find.text('PASTE'), findsOneWidget);
|
expect(find.text('PASTE'), findsOneWidget);
|
||||||
|
|
||||||
// Hide the menu again.
|
// Hide the menu again.
|
||||||
@ -983,7 +986,7 @@ void main() {
|
|||||||
controller.text = 'blah';
|
controller.text = 'blah';
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
expect(state.showToolbar(), true);
|
expect(state.showToolbar(), true);
|
||||||
await tester.pump();
|
await tester.pumpAndSettle();
|
||||||
expect(find.text('PASTE'), findsOneWidget);
|
expect(find.text('PASTE'), findsOneWidget);
|
||||||
}, skip: isBrowser);
|
}, skip: isBrowser);
|
||||||
|
|
||||||
@ -1023,7 +1026,7 @@ void main() {
|
|||||||
|
|
||||||
// Should be able to show the toolbar.
|
// Should be able to show the toolbar.
|
||||||
expect(state.showToolbar(), true);
|
expect(state.showToolbar(), true);
|
||||||
await tester.pump();
|
await tester.pumpAndSettle();
|
||||||
expect(find.text('PASTE'), findsOneWidget);
|
expect(find.text('PASTE'), findsOneWidget);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -1192,7 +1195,7 @@ void main() {
|
|||||||
final Finder textFinder = find.byType(EditableText);
|
final Finder textFinder = find.byType(EditableText);
|
||||||
await tester.longPress(textFinder);
|
await tester.longPress(textFinder);
|
||||||
tester.state<EditableTextState>(textFinder).showToolbar();
|
tester.state<EditableTextState>(textFinder).showToolbar();
|
||||||
await tester.pump();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
await tester.tap(find.text('PASTE'));
|
await tester.tap(find.text('PASTE'));
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
@ -2412,12 +2415,13 @@ void main() {
|
|||||||
|
|
||||||
controls = MockTextSelectionControls();
|
controls = MockTextSelectionControls();
|
||||||
when(controls.buildHandle(any, any, any)).thenReturn(Container());
|
when(controls.buildHandle(any, any, any)).thenReturn(Container());
|
||||||
when(controls.buildToolbar(any, any, any, any, any, any))
|
when(controls.buildToolbar(any, any, any, any, any, any, any))
|
||||||
.thenReturn(Container());
|
.thenReturn(Container());
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('are exposed', (WidgetTester tester) async {
|
testWidgets('are exposed', (WidgetTester tester) async {
|
||||||
final SemanticsTester semantics = SemanticsTester(tester);
|
final SemanticsTester semantics = SemanticsTester(tester);
|
||||||
|
addTearDown(semantics.dispose);
|
||||||
|
|
||||||
when(controls.canCopy(any)).thenReturn(false);
|
when(controls.canCopy(any)).thenReturn(false);
|
||||||
when(controls.canCut(any)).thenReturn(false);
|
when(controls.canCut(any)).thenReturn(false);
|
||||||
@ -2457,6 +2461,7 @@ void main() {
|
|||||||
when(controls.canCopy(any)).thenReturn(false);
|
when(controls.canCopy(any)).thenReturn(false);
|
||||||
when(controls.canPaste(any)).thenReturn(true);
|
when(controls.canPaste(any)).thenReturn(true);
|
||||||
await _buildApp(controls, tester);
|
await _buildApp(controls, tester);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
expect(
|
expect(
|
||||||
semantics,
|
semantics,
|
||||||
includesNodeWith(
|
includesNodeWith(
|
||||||
@ -2504,8 +2509,6 @@ void main() {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
semantics.dispose();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('can copy/cut/paste with a11y', (WidgetTester tester) async {
|
testWidgets('can copy/cut/paste with a11y', (WidgetTester tester) async {
|
||||||
@ -2564,7 +2567,7 @@ void main() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
owner.performAction(expectedNodeId, SemanticsAction.copy);
|
owner.performAction(expectedNodeId, SemanticsAction.copy);
|
||||||
verify(controls.handleCopy(any)).called(1);
|
verify(controls.handleCopy(any, any)).called(1);
|
||||||
|
|
||||||
owner.performAction(expectedNodeId, SemanticsAction.cut);
|
owner.performAction(expectedNodeId, SemanticsAction.cut);
|
||||||
verify(controls.handleCut(any)).called(1);
|
verify(controls.handleCut(any)).called(1);
|
||||||
|
Loading…
Reference in New Issue
Block a user