mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
iOS Text Selection Menu Overflow (#54140)
Adds the ability for the iOS text selection menu to handle items that are too wide for the screen.
This commit is contained in:
parent
57dd045cef
commit
f646e26e90
@ -2,6 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:collection';
|
||||
import 'dart:math' as math;
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
@ -42,7 +43,6 @@ const Radius _kToolbarBorderRadius = Radius.circular(8);
|
||||
const Color _kToolbarBackgroundColor = Color(0xEB202020);
|
||||
const Color _kToolbarDividerColor = Color(0xFF808080);
|
||||
|
||||
|
||||
const TextStyle _kToolbarButtonFontStyle = TextStyle(
|
||||
inherit: false,
|
||||
fontSize: 14.0,
|
||||
@ -51,6 +51,14 @@ const TextStyle _kToolbarButtonFontStyle = TextStyle(
|
||||
color: CupertinoColors.white,
|
||||
);
|
||||
|
||||
const TextStyle _kToolbarButtonDisabledFontStyle = TextStyle(
|
||||
inherit: false,
|
||||
fontSize: 14.0,
|
||||
letterSpacing: -0.15,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: CupertinoColors.inactiveGray,
|
||||
);
|
||||
|
||||
// Eyeballed value.
|
||||
const EdgeInsets _kToolbarButtonPadding = EdgeInsets.symmetric(vertical: 10.0, horizontal: 18.0);
|
||||
|
||||
@ -331,8 +339,6 @@ class _CupertinoTextSelectionControls extends TextSelectionControls {
|
||||
: endpoints.last.point.dy + _kToolbarContentDistance;
|
||||
|
||||
final List<Widget> items = <Widget>[];
|
||||
final Widget onePhysicalPixelVerticalDivider =
|
||||
SizedBox(width: 1.0 / MediaQuery.of(context).devicePixelRatio);
|
||||
final CupertinoLocalizations localizations = CupertinoLocalizations.of(context);
|
||||
final EdgeInsets arrowPadding = isArrowPointingDown
|
||||
? EdgeInsets.only(bottom: _kToolbarArrowSize.height)
|
||||
@ -347,12 +353,12 @@ class _CupertinoTextSelectionControls extends TextSelectionControls {
|
||||
return;
|
||||
}
|
||||
|
||||
if (items.isNotEmpty) {
|
||||
items.add(onePhysicalPixelVerticalDivider);
|
||||
}
|
||||
|
||||
items.add(CupertinoButton(
|
||||
child: Text(text, style: _kToolbarButtonFontStyle),
|
||||
child: Text(
|
||||
text,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: _kToolbarButtonFontStyle,
|
||||
),
|
||||
color: _kToolbarBackgroundColor,
|
||||
minSize: _kToolbarHeight,
|
||||
padding: _kToolbarButtonPadding.add(arrowPadding),
|
||||
@ -371,9 +377,9 @@ class _CupertinoTextSelectionControls extends TextSelectionControls {
|
||||
barTopY: localBarTopY + globalEditableRegion.top,
|
||||
arrowTipX: arrowTipX,
|
||||
isArrowPointingDown: isArrowPointingDown,
|
||||
child: items.isEmpty ? null : DecoratedBox(
|
||||
decoration: const BoxDecoration(color: _kToolbarDividerColor),
|
||||
child: Row(mainAxisSize: MainAxisSize.min, children: items),
|
||||
child: items.isEmpty ? null : _CupertinoTextSelectionToolbarContent(
|
||||
isArrowPointingDown: isArrowPointingDown,
|
||||
children: items,
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -446,5 +452,686 @@ class _CupertinoTextSelectionControls extends TextSelectionControls {
|
||||
}
|
||||
}
|
||||
|
||||
// Renders the content of the selection menu and maintains the page state.
|
||||
class _CupertinoTextSelectionToolbarContent extends StatefulWidget {
|
||||
const _CupertinoTextSelectionToolbarContent({
|
||||
Key key,
|
||||
@required this.children,
|
||||
@required this.isArrowPointingDown,
|
||||
}) : assert(children != null),
|
||||
// This ignore is used because .isNotEmpty isn't compatible with const.
|
||||
assert(children.length > 0), // ignore: prefer_is_empty
|
||||
super(key: key);
|
||||
|
||||
final List<Widget> children;
|
||||
final bool isArrowPointingDown;
|
||||
|
||||
@override
|
||||
_CupertinoTextSelectionToolbarContentState createState() => _CupertinoTextSelectionToolbarContentState();
|
||||
}
|
||||
|
||||
class _CupertinoTextSelectionToolbarContentState extends State<_CupertinoTextSelectionToolbarContent> with TickerProviderStateMixin {
|
||||
// Controls the fading of the buttons within the menu during page transitions.
|
||||
AnimationController _controller;
|
||||
int _page = 0;
|
||||
int _nextPage;
|
||||
|
||||
void _handleNextPage() {
|
||||
_controller.reverse();
|
||||
_controller.addStatusListener(_statusListener);
|
||||
_nextPage = _page + 1;
|
||||
}
|
||||
|
||||
void _handlePreviousPage() {
|
||||
_controller.reverse();
|
||||
_controller.addStatusListener(_statusListener);
|
||||
_nextPage = _page - 1;
|
||||
}
|
||||
|
||||
void _statusListener(AnimationStatus status) {
|
||||
if (status != AnimationStatus.dismissed) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_page = _nextPage;
|
||||
_nextPage = null;
|
||||
});
|
||||
_controller.forward();
|
||||
_controller.removeStatusListener(_statusListener);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
value: 1.0,
|
||||
vsync: this,
|
||||
// This was eyeballed on a physical iOS device running iOS 13.
|
||||
duration: const Duration(milliseconds: 150),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(_CupertinoTextSelectionToolbarContent oldWidget) {
|
||||
// If the children are changing, the current page should be reset.
|
||||
if (widget.children != oldWidget.children) {
|
||||
_page = 0;
|
||||
_nextPage = null;
|
||||
_controller.forward();
|
||||
_controller.removeStatusListener(_statusListener);
|
||||
}
|
||||
super.didUpdateWidget(oldWidget);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final EdgeInsets arrowPadding = widget.isArrowPointingDown
|
||||
? EdgeInsets.only(bottom: _kToolbarArrowSize.height)
|
||||
: EdgeInsets.only(top: _kToolbarArrowSize.height);
|
||||
|
||||
return DecoratedBox(
|
||||
decoration: const BoxDecoration(color: _kToolbarDividerColor),
|
||||
child: FadeTransition(
|
||||
opacity: _controller,
|
||||
child: _CupertinoTextSelectionToolbarItems(
|
||||
page: _page,
|
||||
backButton: CupertinoButton(
|
||||
borderRadius: null,
|
||||
color: _kToolbarBackgroundColor,
|
||||
minSize: _kToolbarHeight,
|
||||
onPressed: _handlePreviousPage,
|
||||
padding: arrowPadding,
|
||||
pressedOpacity: 0.7,
|
||||
child: const Text('◀', style: _kToolbarButtonFontStyle),
|
||||
),
|
||||
dividerWidth: 1.0 / MediaQuery.of(context).devicePixelRatio,
|
||||
nextButton: CupertinoButton(
|
||||
borderRadius: null,
|
||||
color: _kToolbarBackgroundColor,
|
||||
minSize: _kToolbarHeight,
|
||||
onPressed: _handleNextPage,
|
||||
padding: arrowPadding,
|
||||
pressedOpacity: 0.7,
|
||||
child: const Text('▶', style: _kToolbarButtonFontStyle),
|
||||
),
|
||||
nextButtonDisabled: CupertinoButton(
|
||||
borderRadius: null,
|
||||
color: _kToolbarBackgroundColor,
|
||||
disabledColor: _kToolbarBackgroundColor,
|
||||
minSize: _kToolbarHeight,
|
||||
onPressed: null,
|
||||
padding: arrowPadding,
|
||||
pressedOpacity: 1.0,
|
||||
child: const Text('▶', style: _kToolbarButtonDisabledFontStyle),
|
||||
),
|
||||
children: widget.children,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// The custom RenderObjectWidget that, together with
|
||||
// _CupertinoTextSelectionToolbarItemsRenderBox and
|
||||
// _CupertinoTextSelectionToolbarItemsElement, paginates the menu items.
|
||||
class _CupertinoTextSelectionToolbarItems extends RenderObjectWidget {
|
||||
_CupertinoTextSelectionToolbarItems({
|
||||
Key key,
|
||||
@required this.page,
|
||||
@required this.children,
|
||||
@required this.backButton,
|
||||
@required this.dividerWidth,
|
||||
@required this.nextButton,
|
||||
@required this.nextButtonDisabled,
|
||||
}) : assert(children != null),
|
||||
assert(children.isNotEmpty),
|
||||
assert(backButton != null),
|
||||
assert(dividerWidth != null),
|
||||
assert(nextButton != null),
|
||||
assert(nextButtonDisabled != null),
|
||||
assert(page != null),
|
||||
super(key: key);
|
||||
|
||||
final Widget backButton;
|
||||
final List<Widget> children;
|
||||
final double dividerWidth;
|
||||
final Widget nextButton;
|
||||
final Widget nextButtonDisabled;
|
||||
final int page;
|
||||
|
||||
@override
|
||||
_CupertinoTextSelectionToolbarItemsRenderBox createRenderObject(BuildContext context) {
|
||||
return _CupertinoTextSelectionToolbarItemsRenderBox(
|
||||
dividerWidth: dividerWidth,
|
||||
page: page,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void updateRenderObject(BuildContext context, _CupertinoTextSelectionToolbarItemsRenderBox renderObject) {
|
||||
renderObject
|
||||
..page = page
|
||||
..dividerWidth = dividerWidth;
|
||||
}
|
||||
|
||||
@override
|
||||
_CupertinoTextSelectionToolbarItemsElement createElement() => _CupertinoTextSelectionToolbarItemsElement(this);
|
||||
}
|
||||
|
||||
// The custom RenderObjectElement that helps paginate the menu items.
|
||||
class _CupertinoTextSelectionToolbarItemsElement extends RenderObjectElement {
|
||||
_CupertinoTextSelectionToolbarItemsElement(
|
||||
_CupertinoTextSelectionToolbarItems widget,
|
||||
) : super(widget);
|
||||
|
||||
List<Element> _children;
|
||||
final Map<_CupertinoTextSelectionToolbarItemsSlot, Element> slotToChild = <_CupertinoTextSelectionToolbarItemsSlot, Element>{};
|
||||
final Map<Element, _CupertinoTextSelectionToolbarItemsSlot> childToSlot = <Element, _CupertinoTextSelectionToolbarItemsSlot>{};
|
||||
|
||||
// We keep a set of forgotten children to avoid O(n^2) work walking _children
|
||||
// repeatedly to remove children.
|
||||
final Set<Element> _forgottenChildren = HashSet<Element>();
|
||||
|
||||
@override
|
||||
_CupertinoTextSelectionToolbarItems get widget => super.widget as _CupertinoTextSelectionToolbarItems;
|
||||
|
||||
@override
|
||||
_CupertinoTextSelectionToolbarItemsRenderBox get renderObject => super.renderObject as _CupertinoTextSelectionToolbarItemsRenderBox;
|
||||
|
||||
void _updateRenderObject(RenderBox child, _CupertinoTextSelectionToolbarItemsSlot slot) {
|
||||
switch (slot) {
|
||||
case _CupertinoTextSelectionToolbarItemsSlot.backButton:
|
||||
renderObject.backButton = child;
|
||||
break;
|
||||
case _CupertinoTextSelectionToolbarItemsSlot.nextButton:
|
||||
renderObject.nextButton = child;
|
||||
break;
|
||||
case _CupertinoTextSelectionToolbarItemsSlot.nextButtonDisabled:
|
||||
renderObject.nextButtonDisabled = child;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void insertChildRenderObject(RenderObject child, dynamic slot) {
|
||||
if (slot is _CupertinoTextSelectionToolbarItemsSlot) {
|
||||
assert(child is RenderBox);
|
||||
assert(slot is _CupertinoTextSelectionToolbarItemsSlot);
|
||||
_updateRenderObject(child as RenderBox, slot);
|
||||
assert(renderObject.childToSlot.containsKey(child));
|
||||
assert(renderObject.slotToChild.containsKey(slot));
|
||||
return;
|
||||
}
|
||||
if (slot is IndexedSlot) {
|
||||
assert(renderObject.debugValidateChild(child));
|
||||
renderObject.insert(child as RenderBox, after: slot?.value?.renderObject as RenderBox);
|
||||
return;
|
||||
}
|
||||
assert(false, 'slot must be _CupertinoTextSelectionToolbarItemsSlot or IndexedSlot');
|
||||
}
|
||||
|
||||
// This is not reachable for children that don't have an IndexedSlot.
|
||||
@override
|
||||
void moveChildRenderObject(RenderObject child, IndexedSlot<Element> slot) {
|
||||
assert(child.parent == renderObject);
|
||||
renderObject.move(child as RenderBox, after: slot?.value?.renderObject as RenderBox);
|
||||
}
|
||||
|
||||
static bool _shouldPaint(Element child) {
|
||||
return (child.renderObject.parentData as ToolbarItemsParentData).shouldPaint;
|
||||
}
|
||||
|
||||
@override
|
||||
void removeChildRenderObject(RenderObject child) {
|
||||
// Check if the child is in a slot.
|
||||
if (renderObject.childToSlot.containsKey(child)) {
|
||||
assert(child is RenderBox);
|
||||
assert(renderObject.childToSlot.containsKey(child));
|
||||
_updateRenderObject(null, renderObject.childToSlot[child]);
|
||||
assert(!renderObject.childToSlot.containsKey(child));
|
||||
assert(!renderObject.slotToChild.containsKey(slot));
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise look for it in the list of children.
|
||||
assert(child.parent == renderObject);
|
||||
renderObject.remove(child as RenderBox);
|
||||
}
|
||||
|
||||
@override
|
||||
void visitChildren(ElementVisitor visitor) {
|
||||
slotToChild.values.forEach(visitor);
|
||||
for (final Element child in _children) {
|
||||
if (!_forgottenChildren.contains(child))
|
||||
visitor(child);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void forgetChild(Element child) {
|
||||
assert(slotToChild.values.contains(child) || _children.contains(child));
|
||||
assert(!_forgottenChildren.contains(child));
|
||||
// Handle forgetting a child in children or in a slot.
|
||||
if (childToSlot.containsKey(child)) {
|
||||
final _CupertinoTextSelectionToolbarItemsSlot slot = childToSlot[child];
|
||||
childToSlot.remove(child);
|
||||
slotToChild.remove(slot);
|
||||
} else {
|
||||
_forgottenChildren.add(child);
|
||||
}
|
||||
super.forgetChild(child);
|
||||
}
|
||||
|
||||
// Mount or update slotted child.
|
||||
void _mountChild(Widget widget, _CupertinoTextSelectionToolbarItemsSlot slot) {
|
||||
final Element oldChild = slotToChild[slot];
|
||||
final Element newChild = updateChild(oldChild, widget, slot);
|
||||
if (oldChild != null) {
|
||||
slotToChild.remove(slot);
|
||||
childToSlot.remove(oldChild);
|
||||
}
|
||||
if (newChild != null) {
|
||||
slotToChild[slot] = newChild;
|
||||
childToSlot[newChild] = slot;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void mount(Element parent, dynamic newSlot) {
|
||||
super.mount(parent, newSlot);
|
||||
// Mount slotted children.
|
||||
_mountChild(widget.backButton, _CupertinoTextSelectionToolbarItemsSlot.backButton);
|
||||
_mountChild(widget.nextButton, _CupertinoTextSelectionToolbarItemsSlot.nextButton);
|
||||
_mountChild(widget.nextButtonDisabled, _CupertinoTextSelectionToolbarItemsSlot.nextButtonDisabled);
|
||||
|
||||
// Mount list children.
|
||||
_children = List<Element>(widget.children.length);
|
||||
Element previousChild;
|
||||
for (int i = 0; i < _children.length; i += 1) {
|
||||
final Element newChild = inflateWidget(widget.children[i], IndexedSlot<Element>(i, previousChild));
|
||||
_children[i] = newChild;
|
||||
previousChild = newChild;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void debugVisitOnstageChildren(ElementVisitor visitor) {
|
||||
// Visit slot children.
|
||||
childToSlot.forEach((Element child, _) {
|
||||
if (!_shouldPaint(child) || _forgottenChildren.contains(child)) {
|
||||
return;
|
||||
}
|
||||
visitor(child);
|
||||
});
|
||||
// Visit list children.
|
||||
_children
|
||||
.where((Element child) => !_forgottenChildren.contains(child) && _shouldPaint(child))
|
||||
.forEach(visitor);
|
||||
}
|
||||
|
||||
@override
|
||||
void update(_CupertinoTextSelectionToolbarItems newWidget) {
|
||||
super.update(newWidget);
|
||||
assert(widget == newWidget);
|
||||
|
||||
// Update slotted children.
|
||||
_mountChild(widget.backButton, _CupertinoTextSelectionToolbarItemsSlot.backButton);
|
||||
_mountChild(widget.nextButton, _CupertinoTextSelectionToolbarItemsSlot.nextButton);
|
||||
_mountChild(widget.nextButtonDisabled, _CupertinoTextSelectionToolbarItemsSlot.nextButtonDisabled);
|
||||
|
||||
// Update list children.
|
||||
_children = updateChildren(_children, widget.children, forgottenChildren: _forgottenChildren);
|
||||
_forgottenChildren.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// The custom RenderBox that helps paginate the menu items.
|
||||
class _CupertinoTextSelectionToolbarItemsRenderBox extends RenderBox with ContainerRenderObjectMixin<RenderBox, ToolbarItemsParentData>, RenderBoxContainerDefaultsMixin<RenderBox, ToolbarItemsParentData> {
|
||||
_CupertinoTextSelectionToolbarItemsRenderBox({
|
||||
@required double dividerWidth,
|
||||
@required int page,
|
||||
}) : assert(dividerWidth != null),
|
||||
assert(page != null),
|
||||
_dividerWidth = dividerWidth,
|
||||
_page = page,
|
||||
super();
|
||||
|
||||
final Map<_CupertinoTextSelectionToolbarItemsSlot, RenderBox> slotToChild = <_CupertinoTextSelectionToolbarItemsSlot, RenderBox>{};
|
||||
final Map<RenderBox, _CupertinoTextSelectionToolbarItemsSlot> childToSlot = <RenderBox, _CupertinoTextSelectionToolbarItemsSlot>{};
|
||||
|
||||
RenderBox _updateChild(RenderBox oldChild, RenderBox newChild, _CupertinoTextSelectionToolbarItemsSlot slot) {
|
||||
if (oldChild != null) {
|
||||
dropChild(oldChild);
|
||||
childToSlot.remove(oldChild);
|
||||
slotToChild.remove(slot);
|
||||
}
|
||||
if (newChild != null) {
|
||||
childToSlot[newChild] = slot;
|
||||
slotToChild[slot] = newChild;
|
||||
adoptChild(newChild);
|
||||
}
|
||||
return newChild;
|
||||
}
|
||||
|
||||
int _page;
|
||||
int get page => _page;
|
||||
set page(int value) {
|
||||
if (value == _page) {
|
||||
return;
|
||||
}
|
||||
_page = value;
|
||||
markNeedsLayout();
|
||||
}
|
||||
|
||||
double _dividerWidth;
|
||||
double get dividerWidth => _dividerWidth;
|
||||
set dividerWidth(double value) {
|
||||
if (value == _dividerWidth) {
|
||||
return;
|
||||
}
|
||||
_dividerWidth = value;
|
||||
markNeedsLayout();
|
||||
}
|
||||
|
||||
RenderBox _backButton;
|
||||
RenderBox get backButton => _backButton;
|
||||
set backButton(RenderBox value) {
|
||||
_backButton = _updateChild(_backButton, value, _CupertinoTextSelectionToolbarItemsSlot.backButton);
|
||||
}
|
||||
|
||||
RenderBox _nextButton;
|
||||
RenderBox get nextButton => _nextButton;
|
||||
set nextButton(RenderBox value) {
|
||||
_nextButton = _updateChild(_nextButton, value, _CupertinoTextSelectionToolbarItemsSlot.nextButton);
|
||||
}
|
||||
|
||||
RenderBox _nextButtonDisabled;
|
||||
RenderBox get nextButtonDisabled => _nextButtonDisabled;
|
||||
set nextButtonDisabled(RenderBox value) {
|
||||
_nextButtonDisabled = _updateChild(_nextButtonDisabled, value, _CupertinoTextSelectionToolbarItemsSlot.nextButtonDisabled);
|
||||
}
|
||||
|
||||
@override
|
||||
void performLayout() {
|
||||
if (firstChild == null) {
|
||||
performResize();
|
||||
return;
|
||||
}
|
||||
|
||||
// Layout slotted children.
|
||||
_backButton.layout(constraints.loosen(), parentUsesSize: true);
|
||||
_nextButton.layout(constraints.loosen(), parentUsesSize: true);
|
||||
_nextButtonDisabled.layout(constraints.loosen(), parentUsesSize: true);
|
||||
|
||||
final double subsequentPageButtonsWidth =
|
||||
_backButton.size.width + _nextButton.size.width;
|
||||
double currentButtonPosition = 0.0;
|
||||
double toolbarWidth; // The width of the whole widget.
|
||||
double firstPageWidth;
|
||||
int currentPage = 0;
|
||||
int i = -1;
|
||||
visitChildren((RenderObject renderObjectChild) {
|
||||
i++;
|
||||
final RenderBox child = renderObjectChild as RenderBox;
|
||||
final ToolbarItemsParentData childParentData =
|
||||
child.parentData as ToolbarItemsParentData;
|
||||
childParentData.shouldPaint = false;
|
||||
|
||||
// Skip slotted children and children on pages after the visible page.
|
||||
if (childToSlot.containsKey(child) || currentPage > _page) {
|
||||
return;
|
||||
}
|
||||
|
||||
double paginationButtonsWidth = 0.0;
|
||||
if (currentPage == 0) {
|
||||
// If this is the last child, it's ok to fit without a forward button.
|
||||
paginationButtonsWidth =
|
||||
i == childCount - 1 ? 0.0 : _nextButton.size.width;
|
||||
} else {
|
||||
paginationButtonsWidth = subsequentPageButtonsWidth;
|
||||
}
|
||||
|
||||
// The width of the menu is set by the first page.
|
||||
child.layout(
|
||||
BoxConstraints.loose(Size(
|
||||
(currentPage == 0 ? constraints.maxWidth : firstPageWidth) - paginationButtonsWidth,
|
||||
constraints.maxHeight,
|
||||
)),
|
||||
parentUsesSize: true,
|
||||
);
|
||||
|
||||
// If this child causes the current page to overflow, move to the next
|
||||
// page and relayout the child.
|
||||
final double currentWidth =
|
||||
currentButtonPosition + paginationButtonsWidth + child.size.width;
|
||||
if (currentWidth > constraints.maxWidth) {
|
||||
currentPage++;
|
||||
currentButtonPosition = _backButton.size.width + dividerWidth;
|
||||
paginationButtonsWidth = _backButton.size.width + _nextButton.size.width;
|
||||
child.layout(
|
||||
BoxConstraints.loose(Size(
|
||||
firstPageWidth - paginationButtonsWidth,
|
||||
constraints.maxHeight,
|
||||
)),
|
||||
parentUsesSize: true,
|
||||
);
|
||||
}
|
||||
childParentData.offset = Offset(currentButtonPosition, 0.0);
|
||||
currentButtonPosition += child.size.width + dividerWidth;
|
||||
childParentData.shouldPaint = currentPage == page;
|
||||
|
||||
if (currentPage == 0) {
|
||||
firstPageWidth = currentButtonPosition + _nextButton.size.width;
|
||||
}
|
||||
if (currentPage == page) {
|
||||
toolbarWidth = currentButtonPosition;
|
||||
}
|
||||
});
|
||||
|
||||
// It shouldn't be possible to navigate beyond the last page.
|
||||
assert(page <= currentPage);
|
||||
|
||||
// Position page nav buttons.
|
||||
if (currentPage > 0) {
|
||||
final ToolbarItemsParentData nextButtonParentData =
|
||||
_nextButton.parentData as ToolbarItemsParentData;
|
||||
final ToolbarItemsParentData nextButtonDisabledParentData =
|
||||
_nextButtonDisabled.parentData as ToolbarItemsParentData;
|
||||
final ToolbarItemsParentData backButtonParentData =
|
||||
_backButton.parentData as ToolbarItemsParentData;
|
||||
// The forward button always shows if there is more than one page, even on
|
||||
// the last page (it's just disabled).
|
||||
if (page == currentPage) {
|
||||
nextButtonDisabledParentData.offset = Offset(toolbarWidth, 0.0);
|
||||
nextButtonDisabledParentData.shouldPaint = true;
|
||||
toolbarWidth += nextButtonDisabled.size.width;
|
||||
} else {
|
||||
nextButtonParentData.offset = Offset(toolbarWidth, 0.0);
|
||||
nextButtonParentData.shouldPaint = true;
|
||||
toolbarWidth += nextButton.size.width;
|
||||
}
|
||||
if (page > 0) {
|
||||
backButtonParentData.offset = Offset.zero;
|
||||
backButtonParentData.shouldPaint = true;
|
||||
// No need to add the width of the back button to toolbarWidth here. It's
|
||||
// already been taken care of when laying out the children to
|
||||
// accommodate the back button.
|
||||
}
|
||||
} else {
|
||||
// No divider for the next button when there's only one page.
|
||||
toolbarWidth -= dividerWidth;
|
||||
}
|
||||
|
||||
size = constraints.constrain(Size(toolbarWidth, _kToolbarHeight));
|
||||
}
|
||||
|
||||
@override
|
||||
void paint(PaintingContext context, Offset offset) {
|
||||
visitChildren((RenderObject renderObjectChild) {
|
||||
final RenderBox child = renderObjectChild as RenderBox;
|
||||
final ToolbarItemsParentData childParentData = child.parentData as ToolbarItemsParentData;
|
||||
|
||||
if (childParentData.shouldPaint) {
|
||||
final Offset childOffset = childParentData.offset + offset;
|
||||
context.paintChild(child, childOffset);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void setupParentData(RenderBox child) {
|
||||
if (child.parentData is! ToolbarItemsParentData) {
|
||||
child.parentData = ToolbarItemsParentData();
|
||||
}
|
||||
}
|
||||
|
||||
// Returns true iff the single child is hit by the given position.
|
||||
static bool hitTestChild(RenderBox child, BoxHitTestResult result, { Offset position }) {
|
||||
if (child == null) {
|
||||
return false;
|
||||
}
|
||||
final ToolbarItemsParentData childParentData =
|
||||
child.parentData as ToolbarItemsParentData;
|
||||
return result.addWithPaintOffset(
|
||||
offset: childParentData.offset,
|
||||
position: position,
|
||||
hitTest: (BoxHitTestResult result, Offset transformed) {
|
||||
assert(transformed == position - childParentData.offset);
|
||||
return child.hitTest(result, position: transformed);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool hitTestChildren(BoxHitTestResult result, { Offset position }) {
|
||||
// Hit test list children.
|
||||
// The x, y parameters have the top left of the node's box as the origin.
|
||||
RenderBox child = lastChild;
|
||||
while (child != null) {
|
||||
final ToolbarItemsParentData childParentData = child.parentData as ToolbarItemsParentData;
|
||||
|
||||
// Don't hit test children that aren't shown.
|
||||
if (!childParentData.shouldPaint) {
|
||||
child = childParentData.previousSibling;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (hitTestChild(child, result, position: position)) {
|
||||
return true;
|
||||
}
|
||||
child = childParentData.previousSibling;
|
||||
}
|
||||
|
||||
// Hit test slot children.
|
||||
if (hitTestChild(backButton, result, position: position)) {
|
||||
return true;
|
||||
}
|
||||
if (hitTestChild(nextButton, result, position: position)) {
|
||||
return true;
|
||||
}
|
||||
if (hitTestChild(nextButtonDisabled, result, position: position)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
void attach(PipelineOwner owner) {
|
||||
// Attach list children.
|
||||
super.attach(owner);
|
||||
|
||||
// Attach slot children.
|
||||
childToSlot.forEach((RenderBox child, _) {
|
||||
child.attach(owner);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void detach() {
|
||||
// Detach list children.
|
||||
super.detach();
|
||||
|
||||
// Detach slot children.
|
||||
childToSlot.forEach((RenderBox child, _) {
|
||||
child.detach();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void redepthChildren() {
|
||||
visitChildren((RenderObject renderObjectChild) {
|
||||
final RenderBox child = renderObjectChild as RenderBox;
|
||||
redepthChild(child);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void visitChildren(RenderObjectVisitor visitor) {
|
||||
// Visit the slotted children.
|
||||
if (_backButton != null) {
|
||||
visitor(_backButton);
|
||||
}
|
||||
if (_nextButton != null) {
|
||||
visitor(_nextButton);
|
||||
}
|
||||
if (_nextButtonDisabled != null) {
|
||||
visitor(_nextButtonDisabled);
|
||||
}
|
||||
// Visit the list children.
|
||||
super.visitChildren(visitor);
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
List<DiagnosticsNode> debugDescribeChildren() {
|
||||
final List<DiagnosticsNode> value = <DiagnosticsNode>[];
|
||||
visitChildren((RenderObject renderObjectChild) {
|
||||
if (renderObjectChild == null) {
|
||||
return;
|
||||
}
|
||||
final RenderBox child = renderObjectChild as RenderBox;
|
||||
if (child == backButton) {
|
||||
value.add(child.toDiagnosticsNode(name: 'back button'));
|
||||
} else if (child == nextButton) {
|
||||
value.add(child.toDiagnosticsNode(name: 'next button'));
|
||||
} else if (child == nextButtonDisabled) {
|
||||
value.add(child.toDiagnosticsNode(name: 'next button disabled'));
|
||||
|
||||
// List children.
|
||||
} else {
|
||||
value.add(child.toDiagnosticsNode(name: 'menu item'));
|
||||
}
|
||||
});
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
// The slots that can be occupied by widgets in
|
||||
// _CupertinoTextSelectionToolbarItems, excluding the list of children.
|
||||
enum _CupertinoTextSelectionToolbarItemsSlot {
|
||||
backButton,
|
||||
nextButton,
|
||||
nextButtonDisabled,
|
||||
}
|
||||
|
||||
/// Text selection controls that follows iOS design conventions.
|
||||
final TextSelectionControls cupertinoTextSelectionControls = _CupertinoTextSelectionControls();
|
||||
|
@ -213,7 +213,7 @@ class _TextSelectionToolbarContainerRenderBox extends RenderProxyBox {
|
||||
child.size.height,
|
||||
));
|
||||
|
||||
final _ToolbarParentData childParentData = child.parentData as _ToolbarParentData;
|
||||
final ToolbarItemsParentData childParentData = child.parentData as ToolbarItemsParentData;
|
||||
childParentData.offset = Offset(
|
||||
size.width - child.size.width,
|
||||
0.0,
|
||||
@ -223,7 +223,7 @@ class _TextSelectionToolbarContainerRenderBox extends RenderProxyBox {
|
||||
// Paint at the offset set in the parent data.
|
||||
@override
|
||||
void paint(PaintingContext context, Offset offset) {
|
||||
final _ToolbarParentData childParentData = child.parentData as _ToolbarParentData;
|
||||
final ToolbarItemsParentData childParentData = child.parentData as ToolbarItemsParentData;
|
||||
context.paintChild(child, childParentData.offset + offset);
|
||||
}
|
||||
|
||||
@ -231,7 +231,7 @@ class _TextSelectionToolbarContainerRenderBox extends RenderProxyBox {
|
||||
@override
|
||||
bool hitTestChildren(BoxHitTestResult result, { Offset position }) {
|
||||
// The x, y parameters have the top left of the node's box as the origin.
|
||||
final _ToolbarParentData childParentData = child.parentData as _ToolbarParentData;
|
||||
final ToolbarItemsParentData childParentData = child.parentData as ToolbarItemsParentData;
|
||||
return result.addWithPaintOffset(
|
||||
offset: childParentData.offset,
|
||||
position: position,
|
||||
@ -244,14 +244,14 @@ class _TextSelectionToolbarContainerRenderBox extends RenderProxyBox {
|
||||
|
||||
@override
|
||||
void setupParentData(RenderBox child) {
|
||||
if (child.parentData is! _ToolbarParentData) {
|
||||
child.parentData = _ToolbarParentData();
|
||||
if (child.parentData is! ToolbarItemsParentData) {
|
||||
child.parentData = ToolbarItemsParentData();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void applyPaintTransform(RenderObject child, Matrix4 transform) {
|
||||
final _ToolbarParentData childParentData = child.parentData as _ToolbarParentData;
|
||||
final ToolbarItemsParentData childParentData = child.parentData as ToolbarItemsParentData;
|
||||
transform.translate(childParentData.offset.dx, childParentData.offset.dy);
|
||||
super.applyPaintTransform(child, transform);
|
||||
}
|
||||
@ -292,24 +292,13 @@ class _TextSelectionToolbarItems extends MultiChildRenderObjectWidget {
|
||||
_TextSelectionToolbarItemsElement createElement() => _TextSelectionToolbarItemsElement(this);
|
||||
}
|
||||
|
||||
class _ToolbarParentData extends ContainerBoxParentData<RenderBox> {
|
||||
/// Whether or not this child is painted.
|
||||
///
|
||||
/// Children in the selection toolbar may be laid out for measurement purposes
|
||||
/// but not painted. This allows these children to be identified.
|
||||
bool shouldPaint;
|
||||
|
||||
@override
|
||||
String toString() => '${super.toString()}; shouldPaint=$shouldPaint';
|
||||
}
|
||||
|
||||
class _TextSelectionToolbarItemsElement extends MultiChildRenderObjectElement {
|
||||
_TextSelectionToolbarItemsElement(
|
||||
MultiChildRenderObjectWidget widget,
|
||||
) : super(widget);
|
||||
|
||||
static bool _shouldPaint(Element child) {
|
||||
return (child.renderObject.parentData as _ToolbarParentData).shouldPaint;
|
||||
return (child.renderObject.parentData as ToolbarItemsParentData).shouldPaint;
|
||||
}
|
||||
|
||||
@override
|
||||
@ -318,7 +307,7 @@ class _TextSelectionToolbarItemsElement extends MultiChildRenderObjectElement {
|
||||
}
|
||||
}
|
||||
|
||||
class _TextSelectionToolbarItemsRenderBox extends RenderBox with ContainerRenderObjectMixin<RenderBox, _ToolbarParentData> {
|
||||
class _TextSelectionToolbarItemsRenderBox extends RenderBox with ContainerRenderObjectMixin<RenderBox, ToolbarItemsParentData> {
|
||||
_TextSelectionToolbarItemsRenderBox({
|
||||
@required bool isAbove,
|
||||
@required bool overflowOpen,
|
||||
@ -425,7 +414,7 @@ class _TextSelectionToolbarItemsRenderBox extends RenderBox with ContainerRender
|
||||
i++;
|
||||
|
||||
final RenderBox child = renderObjectChild as RenderBox;
|
||||
final _ToolbarParentData childParentData = child.parentData as _ToolbarParentData;
|
||||
final ToolbarItemsParentData childParentData = child.parentData as ToolbarItemsParentData;
|
||||
|
||||
// Handle placing the navigation button after iterating all children.
|
||||
if (renderObjectChild == navButton) {
|
||||
@ -457,7 +446,7 @@ class _TextSelectionToolbarItemsRenderBox extends RenderBox with ContainerRender
|
||||
});
|
||||
|
||||
// Place the navigation button if needed.
|
||||
final _ToolbarParentData navButtonParentData = navButton.parentData as _ToolbarParentData;
|
||||
final ToolbarItemsParentData navButtonParentData = navButton.parentData as ToolbarItemsParentData;
|
||||
if (_shouldPaintChild(firstChild, 0)) {
|
||||
navButtonParentData.shouldPaint = true;
|
||||
if (overflowOpen) {
|
||||
@ -495,7 +484,7 @@ class _TextSelectionToolbarItemsRenderBox extends RenderBox with ContainerRender
|
||||
void paint(PaintingContext context, Offset offset) {
|
||||
visitChildren((RenderObject renderObjectChild) {
|
||||
final RenderBox child = renderObjectChild as RenderBox;
|
||||
final _ToolbarParentData childParentData = child.parentData as _ToolbarParentData;
|
||||
final ToolbarItemsParentData childParentData = child.parentData as ToolbarItemsParentData;
|
||||
if (!childParentData.shouldPaint) {
|
||||
return;
|
||||
}
|
||||
@ -506,8 +495,8 @@ class _TextSelectionToolbarItemsRenderBox extends RenderBox with ContainerRender
|
||||
|
||||
@override
|
||||
void setupParentData(RenderBox child) {
|
||||
if (child.parentData is! _ToolbarParentData) {
|
||||
child.parentData = _ToolbarParentData();
|
||||
if (child.parentData is! ToolbarItemsParentData) {
|
||||
child.parentData = ToolbarItemsParentData();
|
||||
}
|
||||
}
|
||||
|
||||
@ -516,7 +505,7 @@ class _TextSelectionToolbarItemsRenderBox extends RenderBox with ContainerRender
|
||||
// The x, y parameters have the top left of the node's box as the origin.
|
||||
RenderBox child = lastChild;
|
||||
while (child != null) {
|
||||
final _ToolbarParentData childParentData = child.parentData as _ToolbarParentData;
|
||||
final ToolbarItemsParentData childParentData = child.parentData as ToolbarItemsParentData;
|
||||
|
||||
// Don't hit test children aren't shown.
|
||||
if (!childParentData.shouldPaint) {
|
||||
|
@ -85,6 +85,22 @@ typedef TextSelectionOverlayChanged = void Function(TextEditingValue value, Rect
|
||||
/// having to store the start position.
|
||||
typedef DragSelectionUpdateCallback = void Function(DragStartDetails startDetails, DragUpdateDetails updateDetails);
|
||||
|
||||
/// ParentData that determines whether or not to paint the corresponding child.
|
||||
///
|
||||
/// Used in the layout of the Cupertino and Material text selection menus, which
|
||||
/// decide whether or not to paint their buttons after laying them out and
|
||||
/// determining where they overflow.
|
||||
class ToolbarItemsParentData extends ContainerBoxParentData<RenderBox> {
|
||||
/// Whether or not this child is painted.
|
||||
///
|
||||
/// Children in the selection toolbar may be laid out for measurement purposes
|
||||
/// but not painted. This allows these children to be identified.
|
||||
bool shouldPaint = false;
|
||||
|
||||
@override
|
||||
String toString() => '${super.toString()}; shouldPaint=$shouldPaint';
|
||||
}
|
||||
|
||||
/// An interface for building the selection UI, to be provided by the
|
||||
/// implementor of the toolbar widget.
|
||||
///
|
||||
|
@ -2,11 +2,66 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:async';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import '../widgets/text.dart' show textOffsetToPosition;
|
||||
|
||||
class _LongCupertinoLocalizationsDelegate extends LocalizationsDelegate<CupertinoLocalizations> {
|
||||
const _LongCupertinoLocalizationsDelegate();
|
||||
|
||||
@override
|
||||
bool isSupported(Locale locale) => locale.languageCode == 'en';
|
||||
|
||||
@override
|
||||
Future<_LongCupertinoLocalizations> load(Locale locale) => _LongCupertinoLocalizations.load(locale);
|
||||
|
||||
@override
|
||||
bool shouldReload(_LongCupertinoLocalizationsDelegate old) => false;
|
||||
|
||||
@override
|
||||
String toString() => '_LongCupertinoLocalizations.delegate(en_US)';
|
||||
}
|
||||
|
||||
class _LongCupertinoLocalizations extends DefaultCupertinoLocalizations {
|
||||
const _LongCupertinoLocalizations();
|
||||
|
||||
@override
|
||||
String get cutButtonLabel => 'Cutttttttttttttttttttttttttttttttttttttttttttt';
|
||||
@override
|
||||
String get copyButtonLabel => 'Copyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy';
|
||||
@override
|
||||
String get pasteButtonLabel => 'Pasteeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee';
|
||||
@override
|
||||
String get selectAllButtonLabel => 'Select Allllllllllllllllllllllllllllllll';
|
||||
|
||||
static Future<_LongCupertinoLocalizations> load(Locale locale) {
|
||||
return SynchronousFuture<_LongCupertinoLocalizations>(const _LongCupertinoLocalizations());
|
||||
}
|
||||
|
||||
static const LocalizationsDelegate<CupertinoLocalizations> delegate = _LongCupertinoLocalizationsDelegate();
|
||||
}
|
||||
|
||||
const _LongCupertinoLocalizations longLocalizations = _LongCupertinoLocalizations();
|
||||
|
||||
void main() {
|
||||
|
||||
// Returns true iff the button is visually enabled.
|
||||
bool appearsEnabled(WidgetTester tester, String text) {
|
||||
final CupertinoButton button = tester.widget<CupertinoButton>(
|
||||
find.ancestor(
|
||||
of: find.text(text),
|
||||
matching: find.byType(CupertinoButton),
|
||||
),
|
||||
);
|
||||
// Disabled buttons have no opacity change when pressed.
|
||||
return button.pressedOpacity < 1.0;
|
||||
}
|
||||
|
||||
group('canSelectAll', () {
|
||||
Widget createEditableText({
|
||||
Key key,
|
||||
@ -98,4 +153,338 @@ void main() {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('Text selection menu overflow (iOS)', () {
|
||||
testWidgets('All menu items show when they fit.', (WidgetTester tester) async {
|
||||
final TextEditingController controller = TextEditingController(text: 'abc def ghi');
|
||||
await tester.pumpWidget(CupertinoApp(
|
||||
home: Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: MediaQuery(
|
||||
data: const MediaQueryData(size: Size(800.0, 600.0)),
|
||||
child: Center(
|
||||
child: CupertinoTextField(
|
||||
controller: controller,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
));
|
||||
|
||||
// Initially, the menu isn't shown at all.
|
||||
expect(find.text('Cut'), findsNothing);
|
||||
expect(find.text('Copy'), findsNothing);
|
||||
expect(find.text('Paste'), findsNothing);
|
||||
expect(find.text('Select All'), findsNothing);
|
||||
expect(find.text('◀'), findsNothing);
|
||||
expect(find.text('▶'), findsNothing);
|
||||
|
||||
// Long press on an empty space to show the selection menu.
|
||||
await tester.longPressAt(textOffsetToPosition(tester, 4));
|
||||
await tester.pump();
|
||||
expect(find.text('Cut'), findsNothing);
|
||||
expect(find.text('Copy'), findsNothing);
|
||||
expect(find.text('Paste'), findsOneWidget);
|
||||
expect(find.text('Select All'), findsOneWidget);
|
||||
expect(find.text('◀'), findsNothing);
|
||||
expect(find.text('▶'), findsNothing);
|
||||
|
||||
// Double tap to select a word and show the full selection menu.
|
||||
final Offset textOffset = textOffsetToPosition(tester, 1);
|
||||
await tester.tapAt(textOffset);
|
||||
await tester.pump(const Duration(milliseconds: 200));
|
||||
await tester.tapAt(textOffset);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// The full menu is shown without the navigation buttons.
|
||||
expect(find.text('Cut'), findsOneWidget);
|
||||
expect(find.text('Copy'), findsOneWidget);
|
||||
expect(find.text('Paste'), findsOneWidget);
|
||||
expect(find.text('Select All'), findsNothing);
|
||||
expect(find.text('◀'), findsNothing);
|
||||
expect(find.text('▶'), findsNothing);
|
||||
}, skip: isBrowser, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }));
|
||||
|
||||
testWidgets('When a menu item doesn\'t fit, a second page is used.', (WidgetTester tester) async {
|
||||
// Set the screen size to more narrow, so that Paste can't fit.
|
||||
tester.binding.window.physicalSizeTestValue = const Size(800, 800);
|
||||
addTearDown(tester.binding.window.clearPhysicalSizeTestValue);
|
||||
|
||||
final TextEditingController controller = TextEditingController(text: 'abc def ghi');
|
||||
await tester.pumpWidget(CupertinoApp(
|
||||
home: Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: MediaQuery(
|
||||
data: const MediaQueryData(size: Size(800.0, 600.0)),
|
||||
child: Center(
|
||||
child: CupertinoTextField(
|
||||
controller: controller,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
));
|
||||
|
||||
// Initially, the menu isn't shown at all.
|
||||
expect(find.text('Cut'), findsNothing);
|
||||
expect(find.text('Copy'), findsNothing);
|
||||
expect(find.text('Paste'), findsNothing);
|
||||
expect(find.text('Select All'), findsNothing);
|
||||
expect(find.text('◀'), findsNothing);
|
||||
expect(find.text('▶'), findsNothing);
|
||||
|
||||
// Double tap to select a word and show the selection menu.
|
||||
final Offset textOffset = textOffsetToPosition(tester, 1);
|
||||
await tester.tapAt(textOffset);
|
||||
await tester.pump(const Duration(milliseconds: 200));
|
||||
await tester.tapAt(textOffset);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// The last button is missing, and a next button is shown.
|
||||
expect(find.text('Cut'), findsOneWidget);
|
||||
expect(find.text('Copy'), findsOneWidget);
|
||||
expect(find.text('Paste'), findsNothing);
|
||||
expect(find.text('Select All'), findsNothing);
|
||||
expect(find.text('◀'), findsNothing);
|
||||
expect(find.text('▶'), findsOneWidget);
|
||||
expect(appearsEnabled(tester, '▶'), true);
|
||||
|
||||
// Tapping the next button shows the overflowing button.
|
||||
await tester.tap(find.text('▶'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Cut'), findsNothing);
|
||||
expect(find.text('Copy'), findsNothing);
|
||||
expect(find.text('Paste'), findsOneWidget);
|
||||
expect(find.text('Select All'), findsNothing);
|
||||
expect(find.text('◀'), findsOneWidget);
|
||||
expect(appearsEnabled(tester, '◀'), true);
|
||||
expect(find.text('▶'), findsOneWidget);
|
||||
expect(appearsEnabled(tester, '▶'), false);
|
||||
|
||||
// Tapping the back button shows the first page again.
|
||||
await tester.tap(find.text('◀'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Cut'), findsOneWidget);
|
||||
expect(find.text('Copy'), findsOneWidget);
|
||||
expect(find.text('Paste'), findsNothing);
|
||||
expect(find.text('Select All'), findsNothing);
|
||||
expect(find.text('◀'), findsNothing);
|
||||
expect(find.text('▶'), findsOneWidget);
|
||||
expect(appearsEnabled(tester, '▶'), true);
|
||||
}, skip: isBrowser, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }));
|
||||
|
||||
testWidgets('A smaller menu puts each button on its own page.', (WidgetTester tester) async {
|
||||
// Set the screen size to more narrow, so that two buttons can't fit on
|
||||
// the same page.
|
||||
tester.binding.window.physicalSizeTestValue = const Size(640, 800);
|
||||
addTearDown(tester.binding.window.clearPhysicalSizeTestValue);
|
||||
|
||||
final TextEditingController controller = TextEditingController(text: 'abc def ghi');
|
||||
await tester.pumpWidget(CupertinoApp(
|
||||
home: Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: MediaQuery(
|
||||
data: const MediaQueryData(size: Size(800.0, 600.0)),
|
||||
child: Center(
|
||||
child: CupertinoTextField(
|
||||
controller: controller,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
));
|
||||
|
||||
// Initially, the menu isn't shown at all.
|
||||
expect(find.byType(CupertinoButton), findsNothing);
|
||||
expect(find.text('Cut'), findsNothing);
|
||||
expect(find.text('Copy'), findsNothing);
|
||||
expect(find.text('Paste'), findsNothing);
|
||||
expect(find.text('Select All'), findsNothing);
|
||||
expect(find.text('◀'), findsNothing);
|
||||
expect(find.text('▶'), findsNothing);
|
||||
|
||||
// Double tap to select a word and show the selection menu.
|
||||
final Offset textOffset = textOffsetToPosition(tester, 1);
|
||||
await tester.tapAt(textOffset);
|
||||
await tester.pump(const Duration(milliseconds: 200));
|
||||
await tester.tapAt(textOffset);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Only the first button fits, and a next button is shown.
|
||||
expect(find.byType(CupertinoButton), findsNWidgets(2));
|
||||
expect(find.text('Cut'), findsOneWidget);
|
||||
expect(find.text('Copy'), findsNothing);
|
||||
expect(find.text('Paste'), findsNothing);
|
||||
expect(find.text('Select All'), findsNothing);
|
||||
expect(find.text('◀'), findsNothing);
|
||||
expect(find.text('▶'), findsOneWidget);
|
||||
expect(appearsEnabled(tester, '▶'), true);
|
||||
|
||||
// Tapping the next button shows Copy.
|
||||
await tester.tap(find.text('▶'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(CupertinoButton), findsNWidgets(3));
|
||||
expect(find.text('Cut'), findsNothing);
|
||||
expect(find.text('Copy'), findsOneWidget);
|
||||
expect(find.text('Paste'), findsNothing);
|
||||
expect(find.text('Select All'), findsNothing);
|
||||
expect(find.text('◀'), findsOneWidget);
|
||||
expect(appearsEnabled(tester, '◀'), true);
|
||||
expect(find.text('▶'), findsOneWidget);
|
||||
expect(appearsEnabled(tester, '▶'), true);
|
||||
|
||||
// Tapping the next button again shows Paste.
|
||||
await tester.tap(find.text('▶'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(CupertinoButton), findsNWidgets(3));
|
||||
expect(find.text('Cut'), findsNothing);
|
||||
expect(find.text('Copy'), findsNothing);
|
||||
expect(find.text('Paste'), findsOneWidget);
|
||||
expect(find.text('Select All'), findsNothing);
|
||||
expect(find.text('◀'), findsOneWidget);
|
||||
expect(appearsEnabled(tester, '◀'), true);
|
||||
expect(find.text('▶'), findsOneWidget);
|
||||
expect(appearsEnabled(tester, '▶'), false);
|
||||
|
||||
// Tapping the back button shows the second page again.
|
||||
await tester.tap(find.text('◀'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(CupertinoButton), findsNWidgets(3));
|
||||
expect(find.text('Cut'), findsNothing);
|
||||
expect(find.text('Copy'), findsOneWidget);
|
||||
expect(find.text('Paste'), findsNothing);
|
||||
expect(find.text('Select All'), findsNothing);
|
||||
expect(find.text('◀'), findsOneWidget);
|
||||
expect(appearsEnabled(tester, '◀'), true);
|
||||
expect(find.text('▶'), findsOneWidget);
|
||||
expect(appearsEnabled(tester, '▶'), true);
|
||||
|
||||
// Tapping the back button again shows the first page again.
|
||||
await tester.tap(find.text('◀'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(CupertinoButton), findsNWidgets(2));
|
||||
expect(find.text('Cut'), findsOneWidget);
|
||||
expect(find.text('Copy'), findsNothing);
|
||||
expect(find.text('Paste'), findsNothing);
|
||||
expect(find.text('Select All'), findsNothing);
|
||||
expect(find.text('◀'), findsNothing);
|
||||
expect(find.text('▶'), findsOneWidget);
|
||||
expect(appearsEnabled(tester, '▶'), true);
|
||||
}, skip: isBrowser, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }));
|
||||
|
||||
testWidgets('Handles very long locale strings', (WidgetTester tester) async {
|
||||
final TextEditingController controller = TextEditingController(text: 'abc def ghi');
|
||||
await tester.pumpWidget(CupertinoApp(
|
||||
locale: const Locale('en', 'us'),
|
||||
localizationsDelegates: const <LocalizationsDelegate<dynamic>>[
|
||||
_LongCupertinoLocalizations.delegate,
|
||||
DefaultWidgetsLocalizations.delegate,
|
||||
DefaultMaterialLocalizations.delegate,
|
||||
],
|
||||
home: Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: MediaQuery(
|
||||
data: const MediaQueryData(size: Size(800.0, 600.0)),
|
||||
child: Center(
|
||||
child: CupertinoTextField(
|
||||
controller: controller,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
));
|
||||
|
||||
// Initially, the menu isn't shown at all.
|
||||
expect(find.text(longLocalizations.cutButtonLabel), findsNothing);
|
||||
expect(find.text(longLocalizations.copyButtonLabel), findsNothing);
|
||||
expect(find.text(longLocalizations.pasteButtonLabel), findsNothing);
|
||||
expect(find.text(longLocalizations.selectAllButtonLabel), findsNothing);
|
||||
expect(find.text('◀'), findsNothing);
|
||||
expect(find.text('▶'), findsNothing);
|
||||
|
||||
// Long press on an empty space to show the selection menu, with only the
|
||||
// paste button visible.
|
||||
await tester.longPressAt(textOffsetToPosition(tester, 4));
|
||||
await tester.pump();
|
||||
expect(find.text(longLocalizations.cutButtonLabel), findsNothing);
|
||||
expect(find.text(longLocalizations.copyButtonLabel), findsNothing);
|
||||
expect(find.text(longLocalizations.pasteButtonLabel), findsOneWidget);
|
||||
expect(find.text(longLocalizations.selectAllButtonLabel), findsNothing);
|
||||
expect(find.text('◀'), findsNothing);
|
||||
expect(find.text('▶'), findsOneWidget);
|
||||
expect(appearsEnabled(tester, '▶'), true);
|
||||
|
||||
// Tap next to go to the second and final page.
|
||||
await tester.tap(find.text('▶'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text(longLocalizations.cutButtonLabel), findsNothing);
|
||||
expect(find.text(longLocalizations.copyButtonLabel), findsNothing);
|
||||
expect(find.text(longLocalizations.pasteButtonLabel), findsNothing);
|
||||
expect(find.text(longLocalizations.selectAllButtonLabel), findsOneWidget);
|
||||
expect(find.text('◀'), findsOneWidget);
|
||||
expect(find.text('▶'), findsOneWidget);
|
||||
expect(appearsEnabled(tester, '◀'), true);
|
||||
expect(appearsEnabled(tester, '▶'), false);
|
||||
|
||||
// Tap select all to show the full selection menu.
|
||||
await tester.tap(find.text(longLocalizations.selectAllButtonLabel));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Only one button fits on each page.
|
||||
expect(find.text(longLocalizations.cutButtonLabel), findsOneWidget);
|
||||
expect(find.text(longLocalizations.copyButtonLabel), findsNothing);
|
||||
expect(find.text(longLocalizations.pasteButtonLabel), findsNothing);
|
||||
expect(find.text(longLocalizations.selectAllButtonLabel), findsNothing);
|
||||
expect(find.text('◀'), findsNothing);
|
||||
expect(find.text('▶'), findsOneWidget);
|
||||
expect(appearsEnabled(tester, '▶'), true);
|
||||
|
||||
// Tap next to go to the second page.
|
||||
await tester.tap(find.text('▶'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text(longLocalizations.cutButtonLabel), findsNothing);
|
||||
expect(find.text(longLocalizations.copyButtonLabel), findsOneWidget);
|
||||
expect(find.text(longLocalizations.pasteButtonLabel), findsNothing);
|
||||
expect(find.text(longLocalizations.selectAllButtonLabel), findsNothing);
|
||||
expect(find.text('◀'), findsOneWidget);
|
||||
expect(find.text('▶'), findsOneWidget);
|
||||
expect(appearsEnabled(tester, '◀'), true);
|
||||
expect(appearsEnabled(tester, '▶'), true);
|
||||
|
||||
// Tap next to go to the third and final page.
|
||||
await tester.tap(find.text('▶'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text(longLocalizations.cutButtonLabel), findsNothing);
|
||||
expect(find.text(longLocalizations.copyButtonLabel), findsNothing);
|
||||
expect(find.text(longLocalizations.pasteButtonLabel), findsOneWidget);
|
||||
expect(find.text(longLocalizations.selectAllButtonLabel), findsNothing);
|
||||
expect(find.text('◀'), findsOneWidget);
|
||||
expect(find.text('▶'), findsOneWidget);
|
||||
expect(appearsEnabled(tester, '◀'), true);
|
||||
expect(appearsEnabled(tester, '▶'), false);
|
||||
|
||||
// Tap back to go to the second page again.
|
||||
await tester.tap(find.text('◀'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text(longLocalizations.cutButtonLabel), findsNothing);
|
||||
expect(find.text(longLocalizations.copyButtonLabel), findsOneWidget);
|
||||
expect(find.text(longLocalizations.pasteButtonLabel), findsNothing);
|
||||
expect(find.text(longLocalizations.selectAllButtonLabel), findsNothing);
|
||||
expect(find.text('◀'), findsOneWidget);
|
||||
expect(find.text('▶'), findsOneWidget);
|
||||
expect(appearsEnabled(tester, '◀'), true);
|
||||
expect(appearsEnabled(tester, '▶'), true);
|
||||
|
||||
// Tap back to go to the first page again.
|
||||
await tester.tap(find.text('◀'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text(longLocalizations.cutButtonLabel), findsOneWidget);
|
||||
expect(find.text(longLocalizations.copyButtonLabel), findsNothing);
|
||||
expect(find.text(longLocalizations.pasteButtonLabel), findsNothing);
|
||||
expect(find.text(longLocalizations.selectAllButtonLabel), findsNothing);
|
||||
expect(find.text('◀'), findsNothing);
|
||||
expect(find.text('▶'), findsOneWidget);
|
||||
expect(appearsEnabled(tester, '▶'), true);
|
||||
}, skip: isBrowser, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }));
|
||||
});
|
||||
}
|
||||
|
@ -17,8 +17,8 @@ import 'package:flutter/gestures.dart' show DragStartBehavior, PointerDeviceKind
|
||||
|
||||
import '../rendering/mock_canvas.dart';
|
||||
import '../widgets/semantics_tester.dart';
|
||||
import '../widgets/text.dart' show findRenderEditable, globalize, textOffsetToPosition;
|
||||
import 'feedback_tester.dart';
|
||||
import 'text.dart' show findRenderEditable, globalize, textOffsetToPosition;
|
||||
|
||||
class MockClipboard {
|
||||
Object _clipboardData = <String, dynamic>{
|
||||
|
@ -6,7 +6,7 @@ import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'text.dart' show findRenderEditable, globalize, textOffsetToPosition;
|
||||
import '../widgets/text.dart' show findRenderEditable, globalize, textOffsetToPosition;
|
||||
|
||||
void main() {
|
||||
group('canSelectAll', () {
|
||||
|
Loading…
Reference in New Issue
Block a user