text_selection.dart 11.2 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5 6
// 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;

7
import 'package:flutter/foundation.dart';
8
import 'package:flutter/widgets.dart';
9

10
import 'debug.dart';
11
import 'material_localizations.dart';
12
import 'text_selection_theme.dart';
13 14
import 'text_selection_toolbar.dart';
import 'text_selection_toolbar_text_button.dart';
15 16
import 'theme.dart';

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

19
// Padding between the toolbar and the anchor.
20
const double _kToolbarContentDistanceBelow = _kHandleSize - 2.0;
21
const double _kToolbarContentDistance = 8.0;
22

23 24 25 26 27 28 29 30 31 32 33
/// Android Material styled text selection handle controls.
///
/// Specifically does not manage the toolbar, which is left to
/// [EditableText.contextMenuBuilder].
@Deprecated(
  'Use `MaterialTextSelectionControls`. '
  'This feature was deprecated after v3.3.0-0.5.pre.',
)
class MaterialTextSelectionHandleControls extends MaterialTextSelectionControls with TextSelectionHandleControls {
}

34
/// Android Material styled text selection controls.
35 36 37
///
/// The [materialTextSelectionControls] global variable has a
/// suitable instance of this class.
38 39 40 41 42 43
class MaterialTextSelectionControls extends TextSelectionControls {
  /// Returns the size of the Material handle.
  @override
  Size getHandleSize(double textLineHeight) => const Size(_kHandleSize, _kHandleSize);

  /// Builder for material-style copy/paste text selection toolbar.
44 45 46 47
  @Deprecated(
    'Use `contextMenuBuilder` instead. '
    'This feature was deprecated after v3.3.0-0.5.pre.',
  )
48 49 50 51 52 53 54 55
  @override
  Widget buildToolbar(
    BuildContext context,
    Rect globalEditableRegion,
    double textLineHeight,
    Offset selectionMidpoint,
    List<TextSelectionPoint> endpoints,
    TextSelectionDelegate delegate,
56
    ValueListenable<ClipboardStatus>? clipboardStatus,
57
    Offset? lastSecondaryTapDownPosition,
58
  ) {
59
    return _TextSelectionControlsToolbar(
60 61 62 63 64 65
      globalEditableRegion: globalEditableRegion,
      textLineHeight: textLineHeight,
      selectionMidpoint: selectionMidpoint,
      endpoints: endpoints,
      delegate: delegate,
      clipboardStatus: clipboardStatus,
66 67
      handleCut: canCut(delegate) ? () => handleCut(delegate) : null,
      handleCopy: canCopy(delegate) ? () => handleCopy(delegate) : null,
68 69 70 71 72 73 74
      handlePaste: canPaste(delegate) ? () => handlePaste(delegate) : null,
      handleSelectAll: canSelectAll(delegate) ? () => handleSelectAll(delegate) : null,
    );
  }

  /// Builder for material-style text selection handles.
  @override
75
  Widget buildHandle(BuildContext context, TextSelectionHandleType type, double textHeight, [VoidCallback? onTap]) {
76 77 78 79 80 81 82 83 84
    final ThemeData theme = Theme.of(context);
    final Color handleColor = TextSelectionTheme.of(context).selectionHandleColor ?? theme.colorScheme.primary;
    final Widget handle = SizedBox(
      width: _kHandleSize,
      height: _kHandleSize,
      child: CustomPaint(
        painter: _TextSelectionHandlePainter(
          color: handleColor,
        ),
85 86 87 88
        child: GestureDetector(
          onTap: onTap,
          behavior: HitTestBehavior.translucent,
        ),
89 90 91 92 93 94
      ),
    );

    // [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.
95 96 97 98 99
    return switch (type) {
      TextSelectionHandleType.left => Transform.rotate(angle: math.pi / 2.0, child: handle), // points up-right
      TextSelectionHandleType.right => handle, // points up-left
      TextSelectionHandleType.collapsed => Transform.rotate(angle: math.pi / 4.0, child: handle), // points up
    };
100 101 102 103 104 105
  }

  /// Gets anchor for material-style text selection handles.
  ///
  /// See [TextSelectionControls.getHandleAnchor].
  @override
106
  Offset getHandleAnchor(TextSelectionHandleType type, double textLineHeight) {
107 108 109 110 111
    return switch (type) {
      TextSelectionHandleType.collapsed => const Offset(_kHandleSize / 2, -4),
      TextSelectionHandleType.left      => const Offset(_kHandleSize, 0),
      TextSelectionHandleType.right     => Offset.zero,
    };
112 113
  }

114 115 116 117
  @Deprecated(
    'Use `contextMenuBuilder` instead. '
    'This feature was deprecated after v3.3.0-0.5.pre.',
  )
118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142
  @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 delegate.selectAllEnabled &&
           value.text.isNotEmpty &&
           !(value.selection.start == 0 && value.selection.end == value.text.length);
  }
}

// The label and callback for the available default text selection menu buttons.
class _TextSelectionToolbarItemData {
  const _TextSelectionToolbarItemData({
    required this.label,
    required this.onPressed,
  });

  final String label;
  final VoidCallback onPressed;
}

// The highest level toolbar widget, built directly by buildToolbar.
class _TextSelectionControlsToolbar extends StatefulWidget {
  const _TextSelectionControlsToolbar({
143
    required this.clipboardStatus,
144 145 146
    required this.delegate,
    required this.endpoints,
    required this.globalEditableRegion,
147 148 149 150
    required this.handleCut,
    required this.handleCopy,
    required this.handlePaste,
    required this.handleSelectAll,
151 152
    required this.selectionMidpoint,
    required this.textLineHeight,
153
  });
154

155
  final ValueListenable<ClipboardStatus>? clipboardStatus;
156 157 158
  final TextSelectionDelegate delegate;
  final List<TextSelectionPoint> endpoints;
  final Rect globalEditableRegion;
159 160 161 162
  final VoidCallback? handleCut;
  final VoidCallback? handleCopy;
  final VoidCallback? handlePaste;
  final VoidCallback? handleSelectAll;
163 164
  final Offset selectionMidpoint;
  final double textLineHeight;
165 166

  @override
167
  _TextSelectionControlsToolbarState createState() => _TextSelectionControlsToolbarState();
168 169
}

170
class _TextSelectionControlsToolbarState extends State<_TextSelectionControlsToolbar> with TickerProviderStateMixin {
171 172 173 174 175 176 177 178 179
  void _onChangedClipboardStatus() {
    setState(() {
      // Inform the widget that the value of clipboardStatus has changed.
    });
  }

  @override
  void initState() {
    super.initState();
180
    widget.clipboardStatus?.addListener(_onChangedClipboardStatus);
181 182
  }

183
  @override
184
  void didUpdateWidget(_TextSelectionControlsToolbar oldWidget) {
185
    super.didUpdateWidget(oldWidget);
186
    if (widget.clipboardStatus != oldWidget.clipboardStatus) {
187 188
      widget.clipboardStatus?.addListener(_onChangedClipboardStatus);
      oldWidget.clipboardStatus?.removeListener(_onChangedClipboardStatus);
189 190 191 192 193
    }
  }

  @override
  void dispose() {
194
    widget.clipboardStatus?.removeListener(_onChangedClipboardStatus);
195
    super.dispose();
196 197
  }

198 199
  @override
  Widget build(BuildContext context) {
200 201 202 203 204 205 206
    // If there are no buttons to be shown, don't render anything.
    if (widget.handleCut == null && widget.handleCopy == null
        && widget.handlePaste == null && widget.handleSelectAll == null) {
      return const SizedBox.shrink();
    }
    // If the paste button is desired, don't render anything until the state of
    // the clipboard is known, since it's used to determine if paste is shown.
207
    if (widget.handlePaste != null
208
        && widget.clipboardStatus?.value == ClipboardStatus.unknown) {
209
      return const SizedBox.shrink();
210 211
    }

212 213 214 215 216 217
    // Calculate the positioning of the menu. It is placed above the selection
    // if there is enough room, or otherwise below.
    final TextSelectionPoint startTextSelectionPoint = widget.endpoints[0];
    final TextSelectionPoint endTextSelectionPoint = widget.endpoints.length > 1
      ? widget.endpoints[1]
      : widget.endpoints[0];
218 219 220
    final double topAmountInEditableRegion = startTextSelectionPoint.point.dy - widget.textLineHeight;
    final double anchorTop = math.max(topAmountInEditableRegion, 0) + widget.globalEditableRegion.top - _kToolbarContentDistance;

221 222
    final Offset anchorAbove = Offset(
      widget.globalEditableRegion.left + widget.selectionMidpoint.dx,
223
      anchorTop,
224 225 226 227 228 229 230 231 232 233
    );
    final Offset anchorBelow = Offset(
      widget.globalEditableRegion.left + widget.selectionMidpoint.dx,
      widget.globalEditableRegion.top + endTextSelectionPoint.point.dy + _kToolbarContentDistanceBelow,
    );

    // Determine which buttons will appear so that the order and total number is
    // known. A button's position in the menu can slightly affect its
    // appearance.
    assert(debugCheckHasMaterialLocalizations(context));
234
    final MaterialLocalizations localizations = MaterialLocalizations.of(context);
235
    final List<_TextSelectionToolbarItemData> itemDatas = <_TextSelectionToolbarItemData>[
236
      if (widget.handleCut != null)
237 238 239 240
        _TextSelectionToolbarItemData(
          label: localizations.cutButtonLabel,
          onPressed: widget.handleCut!,
        ),
241
      if (widget.handleCopy != null)
242 243 244 245
        _TextSelectionToolbarItemData(
          label: localizations.copyButtonLabel,
          onPressed: widget.handleCopy!,
        ),
246
      if (widget.handlePaste != null
247
          && widget.clipboardStatus?.value == ClipboardStatus.pasteable)
248 249 250 251
        _TextSelectionToolbarItemData(
          label: localizations.pasteButtonLabel,
          onPressed: widget.handlePaste!,
        ),
252
      if (widget.handleSelectAll != null)
253 254 255 256
        _TextSelectionToolbarItemData(
          label: localizations.selectAllButtonLabel,
          onPressed: widget.handleSelectAll!,
        ),
257
    ];
258

259
    // If there is no option available, build an empty widget.
260
    if (itemDatas.isEmpty) {
261
      return const SizedBox.shrink();
262 263
    }

264 265 266 267 268 269 270 271
    return TextSelectionToolbar(
      anchorAbove: anchorAbove,
      anchorBelow: anchorBelow,
      children: itemDatas.asMap().entries.map((MapEntry<int, _TextSelectionToolbarItemData> entry) {
        return TextSelectionToolbarTextButton(
          padding: TextSelectionToolbarTextButton.getPadding(entry.key, itemDatas.length),
          onPressed: entry.value.onPressed,
          child: Text(entry.value.label),
272
        );
273
      }).toList(),
274
    );
275 276 277
  }
}

278
/// Draws a single text selection handle which points up and to the left.
279
class _TextSelectionHandlePainter extends CustomPainter {
280
  _TextSelectionHandlePainter({ required this.color });
281 282 283 284 285

  final Color color;

  @override
  void paint(Canvas canvas, Size size) {
286
    final Paint paint = Paint()..color = color;
287
    final double radius = size.width/2.0;
288 289 290 291
    final Rect circle = Rect.fromCircle(center: Offset(radius, radius), radius: radius);
    final Rect point = Rect.fromLTWH(0.0, 0.0, radius, radius);
    final Path path = Path()..addOval(circle)..addRect(point);
    canvas.drawPath(path, paint);
292 293 294 295 296 297 298 299
  }

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

300 301 302
// TODO(justinmc): Deprecate this after TextSelectionControls.buildToolbar is
// deleted, when users should migrate back to materialTextSelectionControls.
// See https://github.com/flutter/flutter/pull/124262
303 304 305
/// Text selection handle controls that follow the Material Design specification.
final TextSelectionControls materialTextSelectionHandleControls = MaterialTextSelectionHandleControls();

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