text_selection.dart 8.4 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
import 'debug.dart';
11 12
import 'flat_button.dart';
import 'material.dart';
13
import 'material_localizations.dart';
14 15
import 'theme.dart';

xster's avatar
xster committed
16
const double _kHandleSize = 22.0;
17

xster's avatar
xster committed
18 19 20
// Minimal padding from all edges of the selection toolbar to all edges of the
// viewport.
const double _kToolbarScreenPadding = 8.0;
21
const double _kToolbarHeight = 44.0;
22 23 24
// Padding when positioning toolbar below selection.
const double _kToolbarContentDistanceBelow = 16.0;
const double _kToolbarContentDistance = 8.0;
25 26 27

/// Manages a copy/paste text selection toolbar.
class _TextSelectionToolbar extends StatelessWidget {
xster's avatar
xster committed
28 29 30 31 32 33 34
  const _TextSelectionToolbar({
    Key key,
    this.handleCut,
    this.handleCopy,
    this.handlePaste,
    this.handleSelectAll,
  }) : super(key: key);
35

xster's avatar
xster committed
36 37 38 39 40
  final VoidCallback handleCut;
  final VoidCallback handleCopy;
  final VoidCallback handlePaste;
  final VoidCallback handleSelectAll;

41 42
  @override
  Widget build(BuildContext context) {
43
    final List<Widget> items = <Widget>[];
44
    final MaterialLocalizations localizations = MaterialLocalizations.of(context);
45

46
    if (handleCut != null)
47
      items.add(FlatButton(child: Text(localizations.cutButtonLabel), onPressed: handleCut));
48
    if (handleCopy != null)
49
      items.add(FlatButton(child: Text(localizations.copyButtonLabel), onPressed: handleCopy));
50
    if (handlePaste != null)
51
      items.add(FlatButton(child: Text(localizations.pasteButtonLabel), onPressed: handlePaste,));
52
    if (handleSelectAll != null)
53
      items.add(FlatButton(child: Text(localizations.selectAllButtonLabel), onPressed: handleSelectAll));
54

55 56 57 58 59
    // If there is no option available, build an empty widget.
    if (items.isEmpty) {
      return Container(width: 0.0, height: 0.0);
    }

60
    return Material(
61
      elevation: 1.0,
62
      child: Container(
63
        height: _kToolbarHeight,
64 65
        child: Row(mainAxisSize: MainAxisSize.min, children: items),
      ),
66 67 68 69 70 71 72
    );
  }
}

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

75 76 77 78 79 80 81 82 83
  /// 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].
84
  final Offset position;
85 86 87 88 89 90 91 92

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

  @override
  Offset getPositionForChild(Size size, Size childSize) {
93 94 95 96
    final Offset globalPosition = globalEditableRegion.topLeft + position;

    double x = globalPosition.dx - childSize.width / 2.0;
    double y = globalPosition.dy - childSize.height;
97 98 99

    if (x < _kToolbarScreenPadding)
      x = _kToolbarScreenPadding;
100 101 102
    else if (x + childSize.width > screenSize.width - _kToolbarScreenPadding)
      x = screenSize.width - childSize.width - _kToolbarScreenPadding;

103 104
    if (y < _kToolbarScreenPadding)
      y = _kToolbarScreenPadding;
105 106
    else if (y + childSize.height > screenSize.height - _kToolbarScreenPadding)
      y = screenSize.height - childSize.height - _kToolbarScreenPadding;
107

108
    return Offset(x, y);
109 110 111 112 113 114 115 116
  }

  @override
  bool shouldRelayout(_TextSelectionToolbarLayout oldDelegate) {
    return position != oldDelegate.position;
  }
}

117
/// Draws a single text selection handle which points up and to the left.
118 119 120 121 122 123 124
class _TextSelectionHandlePainter extends CustomPainter {
  _TextSelectionHandlePainter({ this.color });

  final Color color;

  @override
  void paint(Canvas canvas, Size size) {
125
    final Paint paint = Paint()..color = color;
126
    final double radius = size.width/2.0;
127 128
    canvas.drawCircle(Offset(radius, radius), radius, paint);
    canvas.drawRect(Rect.fromLTWH(0.0, 0.0, radius, radius), paint);
129 130 131 132 133 134 135 136
  }

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

137
class _MaterialTextSelectionControls extends TextSelectionControls {
138
  /// Returns the size of the Material handle.
139
  @override
140
  Size getHandleSize(double textLineHeight) => const Size(_kHandleSize, _kHandleSize);
141

142 143
  /// Builder for material-style copy/paste text selection toolbar.
  @override
144 145 146
  Widget buildToolbar(
    BuildContext context,
    Rect globalEditableRegion,
147
    double textLineHeight,
148 149 150 151
    Offset position,
    List<TextSelectionPoint> endpoints,
    TextSelectionDelegate delegate,
  ) {
152
    assert(debugCheckHasMediaQuery(context));
153
    assert(debugCheckHasMaterialLocalizations(context));
154 155 156 157

    // The toolbar should appear below the TextField
    // when there is not enough space above the TextField to show it.
    final TextSelectionPoint startTextSelectionPoint = endpoints[0];
158 159 160 161 162 163 164 165 166 167
    final double toolbarHeightNeeded = MediaQuery.of(context).padding.top
      + _kToolbarScreenPadding
      + _kToolbarHeight
      + _kToolbarContentDistance;
    final double availableHeight = globalEditableRegion.top + endpoints.first.point.dy - textLineHeight;
    final bool fitsAbove = toolbarHeightNeeded <= availableHeight;
    final double y = fitsAbove
        ? startTextSelectionPoint.point.dy - _kToolbarContentDistance - textLineHeight
        : startTextSelectionPoint.point.dy + _kToolbarHeight + _kToolbarContentDistanceBelow;
    final Offset preciseMidpoint = Offset(position.dx, y);
168

169 170 171 172
    return ConstrainedBox(
      constraints: BoxConstraints.tight(globalEditableRegion.size),
      child: CustomSingleChildLayout(
        delegate: _TextSelectionToolbarLayout(
173 174
          MediaQuery.of(context).size,
          globalEditableRegion,
175
          preciseMidpoint,
176
        ),
177
        child: _TextSelectionToolbar(
178 179 180 181
          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
182
        ),
183
      ),
184 185 186 187 188
    );
  }

  /// Builder for material-style text selection handles.
  @override
xster's avatar
xster committed
189
  Widget buildHandle(BuildContext context, TextSelectionHandleType type, double textHeight) {
190 191 192 193 194 195
    final Widget handle = SizedBox(
      width: _kHandleSize,
      height: _kHandleSize,
      child: CustomPaint(
        painter: _TextSelectionHandlePainter(
          color: Theme.of(context).textSelectionHandleColor
196 197
        ),
      ),
198 199 200 201 202 203
    );

    // [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) {
204
      case TextSelectionHandleType.left: // points up-right
205 206
        return Transform.rotate(
          angle: math.pi / 2.0,
207
          child: handle,
208
        );
209
      case TextSelectionHandleType.right: // points up-left
210
        return handle;
211
      case TextSelectionHandleType.collapsed: // points up
212 213
        return Transform.rotate(
          angle: math.pi / 4.0,
214
          child: handle,
215 216 217 218
        );
    }
    assert(type != null);
    return null;
219
  }
220

221 222 223 224 225 226 227 228 229 230 231 232 233 234 235
  /// Gets anchor for material-style text selection handles.
  ///
  /// See [TextSelectionControls.getHandleAnchor].
  @override
  Offset getHandleAnchor(TextSelectionHandleType type, double textLineHeight) {
    switch (type) {
      case TextSelectionHandleType.left:
        return const Offset(_kHandleSize, 0);
      case TextSelectionHandleType.right:
        return Offset.zero;
      default:
        return const Offset(_kHandleSize / 2, -4);
    }
  }

236 237 238 239 240 241 242 243
  @override
  bool canSelectAll(TextSelectionDelegate delegate) {
    // Android allows SelectAll when selection is not collapsed, unless
    // everything has already been selected.
    final TextEditingValue value = delegate.textEditingValue;
    return value.text.isNotEmpty &&
      !(value.selection.start == 0 && value.selection.end == value.text.length);
  }
244
}
245

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