| // Copyright 2016 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 'flat_button.dart'; |
| import 'material.dart'; |
| import 'material_localizations.dart'; |
| import 'theme.dart'; |
| |
| const double _kHandleSize = 22.0; |
| // 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. |
| 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 MaterialLocalizations localizations = MaterialLocalizations.of(context); |
| |
| if (!value.selection.isCollapsed) { |
| items.add(new FlatButton(child: new Text(localizations.cutButtonLabel), onPressed: handleCut)); |
| items.add(new FlatButton(child: new Text(localizations.copyButtonLabel), onPressed: handleCopy)); |
| } |
| items.add(new FlatButton( |
| child: new Text(localizations.pasteButtonLabel), |
| // TODO(https://github.com/flutter/flutter/issues/11254): |
| // This should probably be grayed-out if there is nothing to paste. |
| onPressed: handlePaste, |
| )); |
| if (value.text.isNotEmpty) { |
| if (value.selection.isCollapsed) |
| items.add(new FlatButton(child: new Text(localizations.selectAllButtonLabel), onPressed: handleSelectAll)); |
| } |
| |
| return new Material( |
| elevation: 1.0, |
| child: new Container( |
| height: 44.0, |
| child: new Row(mainAxisSize: MainAxisSize.min, children: items) |
| ) |
| ); |
| } |
| } |
| |
| /// 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 position != oldDelegate.position; |
| } |
| } |
| |
| /// Draws a single text selection handle. The [type] determines where the handle |
| /// points (e.g. the [left] handle points up and to the right). |
| class _TextSelectionHandlePainter extends CustomPainter { |
| _TextSelectionHandlePainter({ this.color }); |
| |
| final Color color; |
| |
| @override |
| void paint(Canvas canvas, Size size) { |
| final Paint paint = new Paint()..color = color; |
| final double radius = size.width/2.0; |
| canvas.drawCircle(new Offset(radius, radius), radius, paint); |
| canvas.drawRect(new Rect.fromLTWH(0.0, 0.0, radius, radius), paint); |
| } |
| |
| @override |
| bool shouldRepaint(_TextSelectionHandlePainter oldPainter) { |
| return color != oldPainter.color; |
| } |
| } |
| |
| class _MaterialTextSelectionControls extends TextSelectionControls { |
| @override |
| Size handleSize = const Size(_kHandleSize, _kHandleSize); |
| |
| /// Builder for material-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 material-style text selection handles. |
| @override |
| Widget buildHandle(BuildContext context, TextSelectionHandleType type, double textHeight) { |
| final Widget handle = new SizedBox( |
| width: _kHandleSize, |
| height: _kHandleSize, |
| child: new CustomPaint( |
| painter: new _TextSelectionHandlePainter( |
| color: Theme.of(context).textSelectionHandleColor |
| ) |
| ) |
| ); |
| |
| // [handle] is a circle, with a rectangle in the top left quadrant of that |
| // circle (an onion pointing to 10:30). We rotate [handle] to point |
| // straight up or up-right depending on the handle type. |
| switch (type) { |
| case TextSelectionHandleType.left: // points up-right |
| return new Transform( |
| transform: new Matrix4.rotationZ(math.PI / 2.0), |
| child: handle |
| ); |
| case TextSelectionHandleType.right: // points up-left |
| return handle; |
| case TextSelectionHandleType.collapsed: // points up |
| return new Transform( |
| transform: new Matrix4.rotationZ(math.PI / 4.0), |
| child: handle |
| ); |
| } |
| assert(type != null); |
| return null; |
| } |
| } |
| |
| /// Text selection controls that follow the Material Design specification. |
| final TextSelectionControls materialTextSelectionControls = new _MaterialTextSelectionControls(); |