iOS text selection (#11224)
Extract common text selection overlay logic from Material to Widget and create a Cupertino version of the overlays
This commit is contained in:
parent
3b35c0c9fc
commit
aa096b50af
@ -17,5 +17,6 @@ export 'src/cupertino/page.dart';
|
|||||||
export 'src/cupertino/scaffold.dart';
|
export 'src/cupertino/scaffold.dart';
|
||||||
export 'src/cupertino/slider.dart';
|
export 'src/cupertino/slider.dart';
|
||||||
export 'src/cupertino/switch.dart';
|
export 'src/cupertino/switch.dart';
|
||||||
|
export 'src/cupertino/text_selection.dart';
|
||||||
export 'src/cupertino/thumb_painter.dart';
|
export 'src/cupertino/thumb_painter.dart';
|
||||||
export 'widgets.dart';
|
export 'widgets.dart';
|
||||||
|
@ -47,8 +47,9 @@ class CupertinoButton extends StatefulWidget {
|
|||||||
this.color,
|
this.color,
|
||||||
this.minSize: 44.0,
|
this.minSize: 44.0,
|
||||||
this.pressedOpacity: 0.1,
|
this.pressedOpacity: 0.1,
|
||||||
|
this.borderRadius: const BorderRadius.all(const Radius.circular(8.0)),
|
||||||
@required this.onPressed,
|
@required this.onPressed,
|
||||||
}) : assert(pressedOpacity >= 0.0 && pressedOpacity <= 1.0);
|
}) : assert(pressedOpacity == null || (pressedOpacity >= 0.0 && pressedOpacity <= 1.0));
|
||||||
|
|
||||||
/// The widget below this widget in the tree.
|
/// The widget below this widget in the tree.
|
||||||
///
|
///
|
||||||
@ -83,9 +84,15 @@ class CupertinoButton extends StatefulWidget {
|
|||||||
/// The opacity that the button will fade to when it is pressed.
|
/// The opacity that the button will fade to when it is pressed.
|
||||||
/// The button will have an opacity of 1.0 when it is not pressed.
|
/// The button will have an opacity of 1.0 when it is not pressed.
|
||||||
///
|
///
|
||||||
/// This defaults to 0.1.
|
/// This defaults to 0.1. If null, opacity will not change on pressed if using
|
||||||
|
/// your own custom effects is desired.
|
||||||
final double pressedOpacity;
|
final double pressedOpacity;
|
||||||
|
|
||||||
|
/// The radius of the button's corners when it has a background color.
|
||||||
|
///
|
||||||
|
/// Defaults to round corners of 8 logical pixels.
|
||||||
|
final BorderRadius borderRadius;
|
||||||
|
|
||||||
/// Whether the button is enabled or disabled. Buttons are disabled by default. To
|
/// Whether the button is enabled or disabled. Buttons are disabled by default. To
|
||||||
/// enable a button, set its [onPressed] property to a non-null value.
|
/// enable a button, set its [onPressed] property to a non-null value.
|
||||||
bool get enabled => onPressed != null;
|
bool get enabled => onPressed != null;
|
||||||
@ -112,7 +119,7 @@ class _CupertinoButtonState extends State<CupertinoButton> with SingleTickerProv
|
|||||||
void _setTween() {
|
void _setTween() {
|
||||||
_opacityTween = new Tween<double>(
|
_opacityTween = new Tween<double>(
|
||||||
begin: 1.0,
|
begin: 1.0,
|
||||||
end: widget.pressedOpacity,
|
end: widget.pressedOpacity ?? 1.0,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -164,7 +171,9 @@ class _CupertinoButtonState extends State<CupertinoButton> with SingleTickerProv
|
|||||||
child: new GestureDetector(
|
child: new GestureDetector(
|
||||||
onTap: widget.onPressed,
|
onTap: widget.onPressed,
|
||||||
child: new ConstrainedBox(
|
child: new ConstrainedBox(
|
||||||
constraints: new BoxConstraints(
|
constraints: widget.minSize == null
|
||||||
|
? const BoxConstraints()
|
||||||
|
: new BoxConstraints(
|
||||||
minWidth: widget.minSize,
|
minWidth: widget.minSize,
|
||||||
minHeight: widget.minSize,
|
minHeight: widget.minSize,
|
||||||
),
|
),
|
||||||
@ -175,17 +184,15 @@ class _CupertinoButtonState extends State<CupertinoButton> with SingleTickerProv
|
|||||||
)),
|
)),
|
||||||
child: new DecoratedBox(
|
child: new DecoratedBox(
|
||||||
decoration: new BoxDecoration(
|
decoration: new BoxDecoration(
|
||||||
borderRadius: const BorderRadius.all(const Radius.circular(8.0)),
|
borderRadius: widget.borderRadius,
|
||||||
color: backgroundColor != null && !enabled
|
color: backgroundColor != null && !enabled
|
||||||
? _kDisabledBackground
|
? _kDisabledBackground
|
||||||
: backgroundColor,
|
: backgroundColor,
|
||||||
),
|
),
|
||||||
child: new Padding(
|
child: new Padding(
|
||||||
padding: widget.padding != null
|
padding: widget.padding ?? (backgroundColor != null
|
||||||
? widget.padding
|
|
||||||
: backgroundColor != null
|
|
||||||
? _kBackgroundButtonPadding
|
? _kBackgroundButtonPadding
|
||||||
: _kButtonPadding,
|
: _kButtonPadding),
|
||||||
child: new Center(
|
child: new Center(
|
||||||
widthFactor: 1.0,
|
widthFactor: 1.0,
|
||||||
heightFactor: 1.0,
|
heightFactor: 1.0,
|
||||||
|
301
packages/flutter/lib/src/cupertino/text_selection.dart
Normal file
301
packages/flutter/lib/src/cupertino/text_selection.dart
Normal file
@ -0,0 +1,301 @@
|
|||||||
|
// Copyright 2017 The Chromium Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:flutter/rendering.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
import 'button.dart';
|
||||||
|
|
||||||
|
// Padding around the line at the edge of the text selection that has 0 width and
|
||||||
|
// the height of the text font.
|
||||||
|
const double _kHandlesPadding = 18.0;
|
||||||
|
// Minimal padding from all edges of the selection toolbar to all edges of the
|
||||||
|
// viewport.
|
||||||
|
const double _kToolbarScreenPadding = 8.0;
|
||||||
|
const double _kToolbarHeight = 36.0;
|
||||||
|
|
||||||
|
const Color _kToolbarBackgroundColor = const Color(0xFF2E2E2E);
|
||||||
|
const Color _kToolbarDividerColor = const Color(0xFFB9B9B9);
|
||||||
|
const Color _kHandlesColor = const Color(0xFF146DDE);
|
||||||
|
|
||||||
|
// This offset is used to determine the center of the selection during a drag.
|
||||||
|
// It's slightly below the center of the text so the finger isn't entirely
|
||||||
|
// covering the text being selected.
|
||||||
|
const Size _kSelectionOffset = const Size(20.0, 30.0);
|
||||||
|
const Size _kToolbarTriangleSize = const Size(18.0, 9.0);
|
||||||
|
const EdgeInsets _kToolbarButtonPadding = const EdgeInsets.symmetric(vertical: 10.0, horizontal: 21.0);
|
||||||
|
const BorderRadius _kToolbarBorderRadius = const BorderRadius.all(const Radius.circular(7.5));
|
||||||
|
|
||||||
|
const TextStyle _kToolbarButtonFontStyle = const TextStyle(
|
||||||
|
fontSize: 14.0,
|
||||||
|
letterSpacing: -0.11,
|
||||||
|
fontWeight: FontWeight.w300,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Paints a triangle below the toolbar.
|
||||||
|
class _TextSelectionToolbarNotchPainter extends CustomPainter {
|
||||||
|
@override
|
||||||
|
void paint(Canvas canvas, Size size) {
|
||||||
|
final Paint paint = new Paint()
|
||||||
|
..color = _kToolbarBackgroundColor
|
||||||
|
..style = PaintingStyle.fill;
|
||||||
|
final Path triangle = new Path()
|
||||||
|
..lineTo(_kToolbarTriangleSize.width / 2, 0.0)
|
||||||
|
..lineTo(0.0, _kToolbarTriangleSize.height)
|
||||||
|
..lineTo(-(_kToolbarTriangleSize.width / 2), 0.0)
|
||||||
|
..close();
|
||||||
|
canvas.drawPath(triangle, paint);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool shouldRepaint(_TextSelectionToolbarNotchPainter oldPainter) => false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Manages a copy/paste text selection toolbar.
|
||||||
|
class _TextSelectionToolbar extends StatelessWidget {
|
||||||
|
const _TextSelectionToolbar({
|
||||||
|
Key key,
|
||||||
|
this.delegate,
|
||||||
|
this.handleCut,
|
||||||
|
this.handleCopy,
|
||||||
|
this.handlePaste,
|
||||||
|
this.handleSelectAll,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
final TextSelectionDelegate delegate;
|
||||||
|
TextEditingValue get value => delegate.textEditingValue;
|
||||||
|
|
||||||
|
final VoidCallback handleCut;
|
||||||
|
final VoidCallback handleCopy;
|
||||||
|
final VoidCallback handlePaste;
|
||||||
|
final VoidCallback handleSelectAll;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final List<Widget> items = <Widget>[];
|
||||||
|
final Widget onePhysicalPixelVerticalDivider =
|
||||||
|
new SizedBox(width: 1.0 / MediaQuery.of(context).devicePixelRatio);
|
||||||
|
|
||||||
|
if (!value.selection.isCollapsed) {
|
||||||
|
items.add(_buildToolbarButton('Cut', handleCut));
|
||||||
|
items.add(onePhysicalPixelVerticalDivider);
|
||||||
|
items.add(_buildToolbarButton('Copy', handleCopy));
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(https://github.com/flutter/flutter/issues/11254):
|
||||||
|
// This should probably be grayed-out if there is nothing to paste.
|
||||||
|
if (items.isNotEmpty)
|
||||||
|
items.add(onePhysicalPixelVerticalDivider);
|
||||||
|
items.add(_buildToolbarButton('Paste', handlePaste));
|
||||||
|
|
||||||
|
if (value.text.isNotEmpty && value.selection.isCollapsed) {
|
||||||
|
items.add(onePhysicalPixelVerticalDivider);
|
||||||
|
items.add(_buildToolbarButton('Select All', handleSelectAll));
|
||||||
|
}
|
||||||
|
|
||||||
|
final Widget triangle = new SizedBox.fromSize(
|
||||||
|
size: _kToolbarTriangleSize,
|
||||||
|
child: new CustomPaint(
|
||||||
|
painter: new _TextSelectionToolbarNotchPainter(),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return new Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: <Widget>[
|
||||||
|
new ClipRRect(
|
||||||
|
borderRadius: _kToolbarBorderRadius,
|
||||||
|
child: new DecoratedBox(
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: _kToolbarDividerColor,
|
||||||
|
),
|
||||||
|
child: new Row(mainAxisSize: MainAxisSize.min, children: items),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// TODO(https://github.com/flutter/flutter/issues/11274):
|
||||||
|
// Position the triangle based on the layout delegate.
|
||||||
|
// And avoid letting the triangle line up with any dividers.
|
||||||
|
triangle,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds a themed [CupertinoButton] for the toolbar.
|
||||||
|
CupertinoButton _buildToolbarButton(String text, VoidCallback onPressed) {
|
||||||
|
return new CupertinoButton(
|
||||||
|
child: new Text(text, style: _kToolbarButtonFontStyle),
|
||||||
|
color: _kToolbarBackgroundColor,
|
||||||
|
minSize: _kToolbarHeight,
|
||||||
|
padding: _kToolbarButtonPadding,
|
||||||
|
borderRadius: null,
|
||||||
|
pressedOpacity: 0.7,
|
||||||
|
onPressed: onPressed,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Centers the toolbar around the given position, ensuring that it remains on
|
||||||
|
/// screen.
|
||||||
|
class _TextSelectionToolbarLayout extends SingleChildLayoutDelegate {
|
||||||
|
_TextSelectionToolbarLayout(this.screenSize, this.globalEditableRegion, this.position);
|
||||||
|
|
||||||
|
/// The size of the screen at the time that the toolbar was last laid out.
|
||||||
|
final Size screenSize;
|
||||||
|
|
||||||
|
/// Size and position of the editing region at the time the toolbar was last
|
||||||
|
/// laid out, in global coordinates.
|
||||||
|
final Rect globalEditableRegion;
|
||||||
|
|
||||||
|
/// Anchor position of the toolbar, relative to the top left of the
|
||||||
|
/// [globalEditableRegion].
|
||||||
|
final Offset position;
|
||||||
|
|
||||||
|
@override
|
||||||
|
BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
|
||||||
|
return constraints.loosen();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Offset getPositionForChild(Size size, Size childSize) {
|
||||||
|
final Offset globalPosition = globalEditableRegion.topLeft + position;
|
||||||
|
|
||||||
|
double x = globalPosition.dx - childSize.width / 2.0;
|
||||||
|
double y = globalPosition.dy - childSize.height;
|
||||||
|
|
||||||
|
if (x < _kToolbarScreenPadding)
|
||||||
|
x = _kToolbarScreenPadding;
|
||||||
|
else if (x + childSize.width > screenSize.width - _kToolbarScreenPadding)
|
||||||
|
x = screenSize.width - childSize.width - _kToolbarScreenPadding;
|
||||||
|
|
||||||
|
if (y < _kToolbarScreenPadding)
|
||||||
|
y = _kToolbarScreenPadding;
|
||||||
|
else if (y + childSize.height > screenSize.height - _kToolbarScreenPadding)
|
||||||
|
y = screenSize.height - childSize.height - _kToolbarScreenPadding;
|
||||||
|
|
||||||
|
return new Offset(x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool shouldRelayout(_TextSelectionToolbarLayout oldDelegate) {
|
||||||
|
return screenSize != oldDelegate.screenSize
|
||||||
|
|| globalEditableRegion != oldDelegate.globalEditableRegion
|
||||||
|
|| position != oldDelegate.position;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draws a single text selection handle with a bar and a ball.
|
||||||
|
///
|
||||||
|
/// Draws from a point of origin somewhere inside the size of the painter
|
||||||
|
/// such that the ball is below the point of origin and the bar is above the
|
||||||
|
/// point of origin.
|
||||||
|
class _TextSelectionHandlePainter extends CustomPainter {
|
||||||
|
_TextSelectionHandlePainter({this.origin});
|
||||||
|
|
||||||
|
final Offset origin;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void paint(Canvas canvas, Size size) {
|
||||||
|
final Paint paint = new Paint()
|
||||||
|
..color = _kHandlesColor
|
||||||
|
..strokeWidth = 2.0;
|
||||||
|
// Draw circle below the origin that slightly overlaps the bar.
|
||||||
|
canvas.drawCircle(origin.translate(0.0, 4.0), 5.5, paint);
|
||||||
|
// Draw up from origin leaving 10 pixels of margin on top.
|
||||||
|
canvas.drawLine(
|
||||||
|
origin,
|
||||||
|
origin.translate(
|
||||||
|
0.0,
|
||||||
|
-(size.height - 2.0 * _kHandlesPadding),
|
||||||
|
),
|
||||||
|
paint,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool shouldRepaint(_TextSelectionHandlePainter oldPainter) => origin != oldPainter.origin;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CupertinoTextSelectionControls extends TextSelectionControls {
|
||||||
|
@override
|
||||||
|
Size handleSize = _kSelectionOffset; // Used for drag selection offset.
|
||||||
|
|
||||||
|
/// Builder for iOS-style copy/paste text selection toolbar.
|
||||||
|
@override
|
||||||
|
Widget buildToolbar(BuildContext context, Rect globalEditableRegion, Offset position, TextSelectionDelegate delegate) {
|
||||||
|
assert(debugCheckHasMediaQuery(context));
|
||||||
|
return new ConstrainedBox(
|
||||||
|
constraints: new BoxConstraints.tight(globalEditableRegion.size),
|
||||||
|
child: new CustomSingleChildLayout(
|
||||||
|
delegate: new _TextSelectionToolbarLayout(
|
||||||
|
MediaQuery.of(context).size,
|
||||||
|
globalEditableRegion,
|
||||||
|
position,
|
||||||
|
),
|
||||||
|
child: new _TextSelectionToolbar(
|
||||||
|
delegate: delegate,
|
||||||
|
handleCut: () => handleCut(delegate),
|
||||||
|
handleCopy: () => handleCopy(delegate),
|
||||||
|
handlePaste: () => handlePaste(delegate),
|
||||||
|
handleSelectAll: () => handleSelectAll(delegate),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builder for iOS text selection edges.
|
||||||
|
@override
|
||||||
|
Widget buildHandle(BuildContext context, TextSelectionHandleType type, double textLineHeight) {
|
||||||
|
// We want a size that's a vertical line the height of the text plus a 18.0
|
||||||
|
// padding in every direction that will constitute the selection drag area.
|
||||||
|
final Size desiredSize = new Size(
|
||||||
|
2.0 * _kHandlesPadding,
|
||||||
|
textLineHeight + 2.0 * _kHandlesPadding
|
||||||
|
);
|
||||||
|
|
||||||
|
final Widget handle = new SizedBox.fromSize(
|
||||||
|
size: desiredSize,
|
||||||
|
child: new CustomPaint(
|
||||||
|
painter: new _TextSelectionHandlePainter(
|
||||||
|
// We give the painter a point of origin that's at the bottom baseline
|
||||||
|
// of the selection cursor position.
|
||||||
|
//
|
||||||
|
// We give it in the form of an offset from the top left of the
|
||||||
|
// SizedBox.
|
||||||
|
origin: new Offset(_kHandlesPadding, textLineHeight + _kHandlesPadding),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// [buildHandle]'s widget is positioned at the selection cursor's bottom
|
||||||
|
// baseline. We transform the handle such that the SizedBox is superimposed
|
||||||
|
// on top of the text selection endpoints.
|
||||||
|
switch (type) {
|
||||||
|
case TextSelectionHandleType.left: // The left handle is upside down on iOS.
|
||||||
|
return new Transform(
|
||||||
|
transform: new Matrix4.rotationZ(math.PI)
|
||||||
|
..translate(-_kHandlesPadding, -_kHandlesPadding),
|
||||||
|
child: handle
|
||||||
|
);
|
||||||
|
case TextSelectionHandleType.right:
|
||||||
|
return new Transform(
|
||||||
|
transform: new Matrix4.translationValues(
|
||||||
|
-_kHandlesPadding,
|
||||||
|
-(textLineHeight + _kHandlesPadding),
|
||||||
|
0.0
|
||||||
|
),
|
||||||
|
child: handle
|
||||||
|
);
|
||||||
|
case TextSelectionHandleType.collapsed: // iOS doesn't draw anything for collapsed selections.
|
||||||
|
return new Container();
|
||||||
|
}
|
||||||
|
assert(type != null);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Text selection controls that follows iOS design conventions.
|
||||||
|
final TextSelectionControls cupertinoTextSelectionControls = new _CupertinoTextSelectionControls();
|
@ -2,6 +2,7 @@
|
|||||||
// Use of this source code is governed by a BSD-style license that can be
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
// found in the LICENSE file.
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'package:flutter/cupertino.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
@ -166,8 +167,9 @@ class TextField extends StatefulWidget {
|
|||||||
/// field.
|
/// field.
|
||||||
final ValueChanged<String> onSubmitted;
|
final ValueChanged<String> onSubmitted;
|
||||||
|
|
||||||
/// Optional input validation and formatting overrides. Formatters are run
|
/// Optional input validation and formatting overrides.
|
||||||
/// in the provided order when the text input changes.
|
///
|
||||||
|
/// Formatters are run in the provided order when the text input changes.
|
||||||
final List<TextInputFormatter> inputFormatters;
|
final List<TextInputFormatter> inputFormatters;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -257,7 +259,9 @@ class _TextFieldState extends State<TextField> {
|
|||||||
maxLines: widget.maxLines,
|
maxLines: widget.maxLines,
|
||||||
cursorColor: themeData.textSelectionColor,
|
cursorColor: themeData.textSelectionColor,
|
||||||
selectionColor: themeData.textSelectionColor,
|
selectionColor: themeData.textSelectionColor,
|
||||||
selectionControls: materialTextSelectionControls,
|
selectionControls: themeData.platform == TargetPlatform.iOS
|
||||||
|
? cupertinoTextSelectionControls
|
||||||
|
: materialTextSelectionControls,
|
||||||
onChanged: widget.onChanged,
|
onChanged: widget.onChanged,
|
||||||
onSubmitted: widget.onSubmitted,
|
onSubmitted: widget.onSubmitted,
|
||||||
onSelectionChanged: (TextSelection _, bool longPress) => _onSelectionChanged(context, longPress),
|
onSelectionChanged: (TextSelection _, bool longPress) => _onSelectionChanged(context, longPress),
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
// Use of this source code is governed by a BSD-style license that can be
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
// found in the LICENSE file.
|
// found in the LICENSE file.
|
||||||
|
|
||||||
import 'dart:async';
|
|
||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
@ -13,32 +12,47 @@ import 'flat_button.dart';
|
|||||||
import 'material.dart';
|
import 'material.dart';
|
||||||
import 'theme.dart';
|
import 'theme.dart';
|
||||||
|
|
||||||
const double _kHandleSize = 22.0; // pixels
|
const double _kHandleSize = 22.0;
|
||||||
const double _kToolbarScreenPadding = 8.0; // pixels
|
// Minimal padding from all edges of the selection toolbar to all edges of the
|
||||||
|
// viewport.
|
||||||
|
const double _kToolbarScreenPadding = 8.0;
|
||||||
|
|
||||||
/// Manages a copy/paste text selection toolbar.
|
/// Manages a copy/paste text selection toolbar.
|
||||||
class _TextSelectionToolbar extends StatelessWidget {
|
class _TextSelectionToolbar extends StatelessWidget {
|
||||||
const _TextSelectionToolbar(this.delegate, {Key key}) : super(key: key);
|
const _TextSelectionToolbar({
|
||||||
|
Key key,
|
||||||
|
this.delegate,
|
||||||
|
this.handleCut,
|
||||||
|
this.handleCopy,
|
||||||
|
this.handlePaste,
|
||||||
|
this.handleSelectAll,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
final TextSelectionDelegate delegate;
|
final TextSelectionDelegate delegate;
|
||||||
TextEditingValue get value => delegate.textEditingValue;
|
TextEditingValue get value => delegate.textEditingValue;
|
||||||
|
|
||||||
|
final VoidCallback handleCut;
|
||||||
|
final VoidCallback handleCopy;
|
||||||
|
final VoidCallback handlePaste;
|
||||||
|
final VoidCallback handleSelectAll;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final List<Widget> items = <Widget>[];
|
final List<Widget> items = <Widget>[];
|
||||||
|
|
||||||
if (!value.selection.isCollapsed) {
|
if (!value.selection.isCollapsed) {
|
||||||
items.add(new FlatButton(child: const Text('CUT'), onPressed: _handleCut));
|
items.add(new FlatButton(child: const Text('CUT'), onPressed: handleCut));
|
||||||
items.add(new FlatButton(child: const Text('COPY'), onPressed: _handleCopy));
|
items.add(new FlatButton(child: const Text('COPY'), onPressed: handleCopy));
|
||||||
}
|
}
|
||||||
items.add(new FlatButton(
|
items.add(new FlatButton(
|
||||||
child: const Text('PASTE'),
|
child: const Text('PASTE'),
|
||||||
// TODO(mpcomplete): This should probably be grayed-out if there is nothing to paste.
|
// TODO(https://github.com/flutter/flutter/issues/11254):
|
||||||
onPressed: _handlePaste
|
// This should probably be grayed-out if there is nothing to paste.
|
||||||
|
onPressed: handlePaste,
|
||||||
));
|
));
|
||||||
if (value.text.isNotEmpty) {
|
if (value.text.isNotEmpty) {
|
||||||
if (value.selection.isCollapsed)
|
if (value.selection.isCollapsed)
|
||||||
items.add(new FlatButton(child: const Text('SELECT ALL'), onPressed: _handleSelectAll));
|
items.add(new FlatButton(child: const Text('SELECT ALL'), onPressed: handleSelectAll));
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Material(
|
return new Material(
|
||||||
@ -49,43 +63,6 @@ class _TextSelectionToolbar extends StatelessWidget {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleCut() {
|
|
||||||
Clipboard.setData(new ClipboardData(text: value.selection.textInside(value.text)));
|
|
||||||
delegate.textEditingValue = new TextEditingValue(
|
|
||||||
text: value.selection.textBefore(value.text) + value.selection.textAfter(value.text),
|
|
||||||
selection: new TextSelection.collapsed(offset: value.selection.start)
|
|
||||||
);
|
|
||||||
delegate.hideToolbar();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _handleCopy() {
|
|
||||||
Clipboard.setData(new ClipboardData(text: value.selection.textInside(value.text)));
|
|
||||||
delegate.textEditingValue = new TextEditingValue(
|
|
||||||
text: value.text,
|
|
||||||
selection: new TextSelection.collapsed(offset: value.selection.end)
|
|
||||||
);
|
|
||||||
delegate.hideToolbar();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<Null> _handlePaste() async {
|
|
||||||
final TextEditingValue value = this.value; // Snapshot the input before using `await`.
|
|
||||||
final ClipboardData data = await Clipboard.getData(Clipboard.kTextPlain);
|
|
||||||
if (data != null) {
|
|
||||||
delegate.textEditingValue = new TextEditingValue(
|
|
||||||
text: value.selection.textBefore(value.text) + data.text + value.selection.textAfter(value.text),
|
|
||||||
selection: new TextSelection.collapsed(offset: value.selection.start + data.text.length)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
delegate.hideToolbar();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _handleSelectAll() {
|
|
||||||
delegate.textEditingValue = new TextEditingValue(
|
|
||||||
text: value.text,
|
|
||||||
selection: new TextSelection(baseOffset: 0, extentOffset: value.text.length)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Centers the toolbar around the given position, ensuring that it remains on
|
/// Centers the toolbar around the given position, ensuring that it remains on
|
||||||
@ -172,14 +149,20 @@ class _MaterialTextSelectionControls extends TextSelectionControls {
|
|||||||
globalEditableRegion,
|
globalEditableRegion,
|
||||||
position,
|
position,
|
||||||
),
|
),
|
||||||
child: new _TextSelectionToolbar(delegate),
|
child: new _TextSelectionToolbar(
|
||||||
|
delegate: delegate,
|
||||||
|
handleCut: () => handleCut(delegate),
|
||||||
|
handleCopy: () => handleCopy(delegate),
|
||||||
|
handlePaste: () => handlePaste(delegate),
|
||||||
|
handleSelectAll: () => handleSelectAll(delegate),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Builder for material-style text selection handles.
|
/// Builder for material-style text selection handles.
|
||||||
@override
|
@override
|
||||||
Widget buildHandle(BuildContext context, TextSelectionHandleType type) {
|
Widget buildHandle(BuildContext context, TextSelectionHandleType type, double textHeight) {
|
||||||
final Widget handle = new SizedBox(
|
final Widget handle = new SizedBox(
|
||||||
width: _kHandleSize,
|
width: _kHandleSize,
|
||||||
height: _kHandleSize,
|
height: _kHandleSize,
|
||||||
|
@ -2,6 +2,8 @@
|
|||||||
// Use of this source code is governed by a BSD-style license that can be
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
// found in the LICENSE file.
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
@ -66,9 +68,14 @@ abstract class TextSelectionDelegate {
|
|||||||
|
|
||||||
/// An interface for building the selection UI, to be provided by the
|
/// An interface for building the selection UI, to be provided by the
|
||||||
/// implementor of the toolbar widget.
|
/// implementor of the toolbar widget.
|
||||||
|
///
|
||||||
|
/// Override text operations such as [handleCut] if needed.
|
||||||
abstract class TextSelectionControls {
|
abstract class TextSelectionControls {
|
||||||
/// Builds a selection handle of the given type.
|
/// Builds a selection handle of the given type.
|
||||||
Widget buildHandle(BuildContext context, TextSelectionHandleType type);
|
///
|
||||||
|
/// The top left corner of this widget is positioned at the bottom of the
|
||||||
|
/// selection position.
|
||||||
|
Widget buildHandle(BuildContext context, TextSelectionHandleType type, double textLineHeight);
|
||||||
|
|
||||||
/// Builds a toolbar near a text selection.
|
/// Builds a toolbar near a text selection.
|
||||||
///
|
///
|
||||||
@ -77,6 +84,59 @@ abstract class TextSelectionControls {
|
|||||||
|
|
||||||
/// Returns the size of the selection handle.
|
/// Returns the size of the selection handle.
|
||||||
Size get handleSize;
|
Size get handleSize;
|
||||||
|
|
||||||
|
void handleCut(TextSelectionDelegate delegate) {
|
||||||
|
final TextEditingValue value = delegate.textEditingValue;
|
||||||
|
Clipboard.setData(new ClipboardData(
|
||||||
|
text: value.selection.textInside(value.text),
|
||||||
|
));
|
||||||
|
delegate.textEditingValue = new TextEditingValue(
|
||||||
|
text: value.selection.textBefore(value.text)
|
||||||
|
+ value.selection.textAfter(value.text),
|
||||||
|
selection: new TextSelection.collapsed(
|
||||||
|
offset: value.selection.start
|
||||||
|
),
|
||||||
|
);
|
||||||
|
delegate.hideToolbar();
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleCopy(TextSelectionDelegate delegate) {
|
||||||
|
final TextEditingValue value = delegate.textEditingValue;
|
||||||
|
Clipboard.setData(new ClipboardData(
|
||||||
|
text: value.selection.textInside(value.text),
|
||||||
|
));
|
||||||
|
delegate.textEditingValue = new TextEditingValue(
|
||||||
|
text: value.text,
|
||||||
|
selection: new TextSelection.collapsed(offset: value.selection.end),
|
||||||
|
);
|
||||||
|
delegate.hideToolbar();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Null> handlePaste(TextSelectionDelegate delegate) async {
|
||||||
|
final TextEditingValue value = delegate.textEditingValue; // Snapshot the input before using `await`.
|
||||||
|
final ClipboardData data = await Clipboard.getData(Clipboard.kTextPlain);
|
||||||
|
if (data != null) {
|
||||||
|
delegate.textEditingValue = new TextEditingValue(
|
||||||
|
text: value.selection.textBefore(value.text)
|
||||||
|
+ data.text
|
||||||
|
+ value.selection.textAfter(value.text),
|
||||||
|
selection: new TextSelection.collapsed(
|
||||||
|
offset: value.selection.start + data.text.length
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
delegate.hideToolbar();
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleSelectAll(TextSelectionDelegate delegate) {
|
||||||
|
delegate.textEditingValue = new TextEditingValue(
|
||||||
|
text: delegate.textEditingValue.text,
|
||||||
|
selection: new TextSelection(
|
||||||
|
baseOffset: 0,
|
||||||
|
extentOffset: delegate.textEditingValue.text.length
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An object that manages a pair of text selection handles.
|
/// An object that manages a pair of text selection handles.
|
||||||
@ -416,7 +476,11 @@ class _TextSelectionHandleOverlayState extends State<_TextSelectionHandleOverlay
|
|||||||
new Positioned(
|
new Positioned(
|
||||||
left: point.dx,
|
left: point.dx,
|
||||||
top: point.dy,
|
top: point.dy,
|
||||||
child: widget.selectionControls.buildHandle(context, type),
|
child: widget.selectionControls.buildHandle(
|
||||||
|
context,
|
||||||
|
type,
|
||||||
|
widget.renderObject.size.height / widget.renderObject.maxLines,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
Loading…
x
Reference in New Issue
Block a user