text_selection.dart 6.44 KB
Newer Older
1 2 3 4 5 6 7
// 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';
8
import 'package:flutter/rendering.dart';
9 10 11

import 'flat_button.dart';
import 'material.dart';
12
import 'material_localizations.dart';
13 14
import 'theme.dart';

xster's avatar
xster committed
15 16 17 18
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;
19 20 21

/// Manages a copy/paste text selection toolbar.
class _TextSelectionToolbar extends StatelessWidget {
xster's avatar
xster committed
22 23 24 25 26 27 28
  const _TextSelectionToolbar({
    Key key,
    this.handleCut,
    this.handleCopy,
    this.handlePaste,
    this.handleSelectAll,
  }) : super(key: key);
29

xster's avatar
xster committed
30 31 32 33 34
  final VoidCallback handleCut;
  final VoidCallback handleCopy;
  final VoidCallback handlePaste;
  final VoidCallback handleSelectAll;

35 36
  @override
  Widget build(BuildContext context) {
37
    final List<Widget> items = <Widget>[];
38
    final MaterialLocalizations localizations = MaterialLocalizations.of(context);
39

40
    if (handleCut != null)
41
      items.add(new FlatButton(child: new Text(localizations.cutButtonLabel), onPressed: handleCut));
42
    if (handleCopy != null)
43
      items.add(new FlatButton(child: new Text(localizations.copyButtonLabel), onPressed: handleCopy));
44 45 46 47
    if (handlePaste != null)
      items.add(new FlatButton(child: new Text(localizations.pasteButtonLabel), onPressed: handlePaste,));
    if (handleSelectAll != null)
      items.add(new FlatButton(child: new Text(localizations.selectAllButtonLabel), onPressed: handleSelectAll));
48 49

    return new Material(
50
      elevation: 1.0,
51 52
      child: new Container(
        height: 44.0,
53
        child: new Row(mainAxisSize: MainAxisSize.min, children: items)
54 55 56 57 58 59 60 61
      )
    );
  }
}

/// Centers the toolbar around the given position, ensuring that it remains on
/// screen.
class _TextSelectionToolbarLayout extends SingleChildLayoutDelegate {
62
  _TextSelectionToolbarLayout(this.screenSize, this.globalEditableRegion, this.position);
63

64 65 66 67 68 69 70 71 72
  /// 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].
73
  final Offset position;
74 75 76 77 78 79 80 81

  @override
  BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
    return constraints.loosen();
  }

  @override
  Offset getPositionForChild(Size size, Size childSize) {
82 83 84 85
    final Offset globalPosition = globalEditableRegion.topLeft + position;

    double x = globalPosition.dx - childSize.width / 2.0;
    double y = globalPosition.dy - childSize.height;
86 87 88

    if (x < _kToolbarScreenPadding)
      x = _kToolbarScreenPadding;
89 90 91
    else if (x + childSize.width > screenSize.width - _kToolbarScreenPadding)
      x = screenSize.width - childSize.width - _kToolbarScreenPadding;

92 93
    if (y < _kToolbarScreenPadding)
      y = _kToolbarScreenPadding;
94 95
    else if (y + childSize.height > screenSize.height - _kToolbarScreenPadding)
      y = screenSize.height - childSize.height - _kToolbarScreenPadding;
96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114

    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) {
115 116
    final Paint paint = new Paint()..color = color;
    final double radius = size.width/2.0;
117
    canvas.drawCircle(new Offset(radius, radius), radius, paint);
118 119 120 121 122 123 124 125 126
    canvas.drawRect(new Rect.fromLTWH(0.0, 0.0, radius, radius), paint);
  }

  @override
  bool shouldRepaint(_TextSelectionHandlePainter oldPainter) {
    return color != oldPainter.color;
  }
}

127 128 129
class _MaterialTextSelectionControls extends TextSelectionControls {
  @override
  Size handleSize = const Size(_kHandleSize, _kHandleSize);
130

131 132
  /// Builder for material-style copy/paste text selection toolbar.
  @override
133
  Widget buildToolbar(BuildContext context, Rect globalEditableRegion, Offset position, TextSelectionDelegate delegate) {
134
    assert(debugCheckHasMediaQuery(context));
135
    return new ConstrainedBox(
136
      constraints: new BoxConstraints.tight(globalEditableRegion.size),
137
      child: new CustomSingleChildLayout(
138 139 140 141 142
        delegate: new _TextSelectionToolbarLayout(
          MediaQuery.of(context).size,
          globalEditableRegion,
          position,
        ),
xster's avatar
xster committed
143
        child: new _TextSelectionToolbar(
144 145 146 147
          handleCut: canCut(delegate) ? () => handleCut(delegate) : null,
          handleCopy: canCopy(delegate) ? () => handleCopy(delegate) : null,
          handlePaste: canPaste(delegate) ? () => handlePaste(delegate) : null,
          handleSelectAll: canSelectAll(delegate) ? () => handleSelectAll(delegate) : null,
xster's avatar
xster committed
148
        ),
149
      )
150 151 152 153 154
    );
  }

  /// Builder for material-style text selection handles.
  @override
xster's avatar
xster committed
155
  Widget buildHandle(BuildContext context, TextSelectionHandleType type, double textHeight) {
156
    final Widget handle = new SizedBox(
157 158 159 160 161 162 163 164 165 166 167 168 169
      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) {
170
      case TextSelectionHandleType.left: // points up-right
171 172 173 174
        return new Transform(
          transform: new Matrix4.rotationZ(math.PI / 2.0),
          child: handle
        );
175
      case TextSelectionHandleType.right: // points up-left
176
        return handle;
177
      case TextSelectionHandleType.collapsed: // points up
178 179 180 181 182 183 184
        return new Transform(
          transform: new Matrix4.rotationZ(math.PI / 4.0),
          child: handle
        );
    }
    assert(type != null);
    return null;
185 186
  }
}
187

Adam Barth's avatar
Adam Barth committed
188
/// Text selection controls that follow the Material Design specification.
189
final TextSelectionControls materialTextSelectionControls = new _MaterialTextSelectionControls();