Commit 6fd7987b authored by Matt Perry's avatar Matt Perry

Text selection UI matches behavior on Android. (#3886)

- Handles appear with tap or long press.
- Toolbar appears with long press on text, or tap on handle.
- Correct toolbar items shown depending on context.
parent 7de612ad
......@@ -37,11 +37,12 @@ class _TextSelectionToolbar extends StatelessWidget {
// TODO(mpcomplete): This should probably be grayed-out if there is nothing to paste.
onPressed: _handlePaste
));
if (value.selection.isCollapsed) {
items.add(new FlatButton(child: new Text('SELECT ALL'), onPressed: _handleSelectAll));
if (value.text.isNotEmpty) {
if (value.selection.isCollapsed)
items.add(new FlatButton(child: new Text('SELECT ALL'), onPressed: _handleSelectAll));
// TODO(mpcomplete): implement `more` menu.
items.add(new IconButton(icon: Icons.more_vert));
}
// TODO(mpcomplete): implement `more` menu.
items.add(new IconButton(icon: Icons.more_vert));
return new Material(
elevation: 1,
......
......@@ -17,7 +17,7 @@ const double _kCaretWidth = 1.0; // pixels
final String _kZeroWidthSpace = new String.fromCharCode(0x200B);
/// Called when the user changes the selection (including cursor location).
typedef void SelectionChangedHandler(TextSelection selection, RenderEditableLine renderObject);
typedef void SelectionChangedHandler(TextSelection selection, RenderEditableLine renderObject, bool longPress);
/// Represents a global screen coordinate of the point in a selection, and the
/// text direction at that point.
......@@ -128,7 +128,7 @@ class RenderEditableLine extends RenderBox {
if (selection.isCollapsed) {
// TODO(mpcomplete): This doesn't work well at an RTL/LTR boundary.
Offset caretOffset = _textPainter.getOffsetForCaret(selection.extent, _caretPrototype);
Point start = new Point(caretOffset.dx, _contentSize.height) + offset;
Point start = new Point(caretOffset.dx, size.height) + offset;
return <TextSelectionPoint>[new TextSelectionPoint(localToGlobal(start), null)];
} else {
List<ui.TextBox> boxes = _textPainter.getBoxesForSelection(selection);
......@@ -212,7 +212,7 @@ class RenderEditableLine extends RenderBox {
_lastTapDownPosition = null;
if (onSelectionChanged != null) {
TextPosition position = _textPainter.getPositionForOffset(globalToLocal(global).toOffset());
onSelectionChanged(new TextSelection.fromPosition(position), this);
onSelectionChanged(new TextSelection.fromPosition(position), this, false);
}
}
......@@ -227,12 +227,15 @@ class RenderEditableLine extends RenderBox {
_longPressPosition = null;
if (onSelectionChanged != null) {
TextPosition position = _textPainter.getPositionForOffset(globalToLocal(global).toOffset());
onSelectionChanged(_selectWordAtOffset(position), this);
onSelectionChanged(_selectWordAtOffset(position), this, true);
}
}
TextSelection _selectWordAtOffset(TextPosition position) {
TextRange word = _textPainter.getWordBoundary(position);
// When long-pressing past the end of the text, we want a collapsed cursor.
if (position.offset >= word.end)
return new TextSelection.fromPosition(position);
return new TextSelection(baseOffset: word.start, extentOffset: word.end);
}
......
......@@ -299,7 +299,7 @@ class RawInputLineState extends ScrollableState<RawInputLine> {
config.onSubmitted(_keyboardClient.inputValue);
}
void _handleSelectionChanged(TextSelection selection, RenderEditableLine renderObject) {
void _handleSelectionChanged(TextSelection selection, RenderEditableLine renderObject, bool longPress) {
// Note that this will show the keyboard for all selection changes on the
// EditableLineWidget, not just changes triggered by user gestures.
requestKeyboard();
......@@ -313,7 +313,7 @@ class RawInputLineState extends ScrollableState<RawInputLine> {
_selectionOverlay = null;
}
if (newInput.text.isNotEmpty && config.selectionHandleBuilder != null) {
if (config.selectionHandleBuilder != null) {
_selectionOverlay = new TextSelectionOverlay(
input: newInput,
context: context,
......@@ -323,7 +323,10 @@ class RawInputLineState extends ScrollableState<RawInputLine> {
handleBuilder: config.selectionHandleBuilder,
toolbarBuilder: config.selectionToolbarBuilder
);
_selectionOverlay.show();
if (newInput.text.isNotEmpty || longPress)
_selectionOverlay.showHandles();
if (longPress)
_selectionOverlay.showToolbar();
}
}
......
......@@ -73,40 +73,49 @@ class TextSelectionOverlay implements TextSelectionDelegate {
/// second is hidden when the selection is collapsed.
List<OverlayEntry> _handles;
/// A copy/paste toolbar.
OverlayEntry _toolbar;
TextSelection get _selection => _input.selection;
/// Shows the handles by inserting them into the [context]'s overlay.
void show() {
void showHandles() {
assert(_handles == null);
_handles = <OverlayEntry>[
new OverlayEntry(builder: (BuildContext c) => _buildOverlay(c, _TextSelectionHandlePosition.start)),
new OverlayEntry(builder: (BuildContext c) => _buildOverlay(c, _TextSelectionHandlePosition.end)),
];
_toolbar = new OverlayEntry(builder: _buildToolbar);
Overlay.of(context, debugRequiredFor: debugRequiredFor).insertAll(_handles);
}
/// Shows the toolbar by inserting it into the [context]'s overlay.
void showToolbar() {
assert(_toolbar == null);
_toolbar = new OverlayEntry(builder: _buildToolbar);
Overlay.of(context, debugRequiredFor: debugRequiredFor).insert(_toolbar);
}
/// Updates the handles after the [selection] has changed.
/// Updates the overlay after the [selection] has changed.
void update(InputValue newInput) {
_input = newInput;
if (_handles == null)
if (_input == newInput)
return;
_handles[0].markNeedsBuild();
_handles[1].markNeedsBuild();
_toolbar.markNeedsBuild();
_input = newInput;
if (_handles != null) {
_handles[0].markNeedsBuild();
_handles[1].markNeedsBuild();
}
_toolbar?.markNeedsBuild();
}
/// Hides the handles.
/// Hides the overlay.
void hide() {
if (_handles == null)
return;
_handles[0].remove();
_handles[1].remove();
_handles = null;
_toolbar.remove();
if (_handles != null) {
_handles[0].remove();
_handles[1].remove();
_handles = null;
}
_toolbar?.remove();
_toolbar = null;
}
......@@ -116,6 +125,7 @@ class TextSelectionOverlay implements TextSelectionDelegate {
return new Container(); // hide the second handle when collapsed
return new _TextSelectionHandleOverlay(
onSelectionHandleChanged: _handleSelectionHandleChanged,
onSelectionHandleTapped: _handleSelectionHandleTapped,
renderObject: renderObject,
selection: _selection,
builder: handleBuilder,
......@@ -143,6 +153,17 @@ class TextSelectionOverlay implements TextSelectionDelegate {
inputValue = _input.copyWith(selection: newSelection, composing: TextRange.empty);
}
void _handleSelectionHandleTapped() {
if (inputValue.selection.isCollapsed) {
if (_toolbar != null) {
_toolbar?.remove();
_toolbar = null;
} else {
showToolbar();
}
}
}
@override
InputValue get inputValue => _input;
......@@ -167,6 +188,7 @@ class _TextSelectionHandleOverlay extends StatefulWidget {
this.position,
this.renderObject,
this.onSelectionHandleChanged,
this.onSelectionHandleTapped,
this.builder
}) : super(key: key);
......@@ -174,6 +196,7 @@ class _TextSelectionHandleOverlay extends StatefulWidget {
final _TextSelectionHandlePosition position;
final RenderEditableLine renderObject;
final ValueChanged<TextSelection> onSelectionHandleChanged;
final VoidCallback onSelectionHandleTapped;
final TextSelectionHandleBuilder builder;
@override
......@@ -217,6 +240,10 @@ class _TextSelectionHandleOverlayState extends State<_TextSelectionHandleOverlay
config.onSelectionHandleChanged(newSelection);
}
void _handleTap() {
config.onSelectionHandleTapped();
}
@override
Widget build(BuildContext context) {
List<TextSelectionPoint> endpoints = config.renderObject.getEndpointsForSelection(config.selection);
......@@ -240,6 +267,7 @@ class _TextSelectionHandleOverlayState extends State<_TextSelectionHandleOverlay
return new GestureDetector(
onHorizontalDragStart: _handleDragStart,
onHorizontalDragUpdate: _handleDragUpdate,
onTap: _handleTap,
child: new Stack(
children: <Widget>[
new Positioned(
......
......@@ -342,9 +342,14 @@ void main() {
enterText(testValue);
tester.pumpWidget(builder());
// Tap the text to bring up the "paste / select all" menu.
// Tap the selection handle to bring up the "paste / select all" menu.
tester.tapAt(textOffsetToPosition(tester, testValue.indexOf('e')));
tester.pumpWidget(builder());
RenderEditableLine renderLine = findRenderEditableLine(tester);
List<TextSelectionPoint> endpoints = renderLine.getEndpointsForSelection(
inputValue.selection);
tester.tapAt(endpoints[0].point + new Offset(1.0, 1.0));
tester.pumpWidget(builder());
// SELECT ALL should select all the text.
tester.tap(find.text('SELECT ALL'));
......@@ -360,6 +365,10 @@ void main() {
// Tap again to bring back the menu.
tester.tapAt(textOffsetToPosition(tester, testValue.indexOf('e')));
tester.pumpWidget(builder());
renderLine = findRenderEditableLine(tester);
endpoints = renderLine.getEndpointsForSelection(inputValue.selection);
tester.tapAt(endpoints[0].point + new Offset(1.0, 1.0));
tester.pumpWidget(builder());
// PASTE right before the 'e'.
tester.tap(find.text('PASTE'));
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment