// Copyright 2014 The Flutter 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/foundation.dart'; import 'package:flutter/widgets.dart'; import 'debug.dart'; import 'material_localizations.dart'; import 'text_selection_theme.dart'; import 'text_selection_toolbar.dart'; import 'text_selection_toolbar_text_button.dart'; import 'theme.dart'; const double _kHandleSize = 22.0; // Padding between the toolbar and the anchor. const double _kToolbarContentDistanceBelow = _kHandleSize - 2.0; const double _kToolbarContentDistance = 8.0; /// 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 { } /// Android Material styled text selection controls. /// /// The [materialTextSelectionControls] global variable has a /// suitable instance of this class. 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. @Deprecated( 'Use `contextMenuBuilder` instead. ' 'This feature was deprecated after v3.3.0-0.5.pre.', ) @override Widget buildToolbar( BuildContext context, Rect globalEditableRegion, double textLineHeight, Offset selectionMidpoint, List<TextSelectionPoint> endpoints, TextSelectionDelegate delegate, ValueListenable<ClipboardStatus>? clipboardStatus, Offset? lastSecondaryTapDownPosition, ) { return _TextSelectionControlsToolbar( globalEditableRegion: globalEditableRegion, textLineHeight: textLineHeight, selectionMidpoint: selectionMidpoint, endpoints: endpoints, delegate: delegate, clipboardStatus: clipboardStatus, handleCut: canCut(delegate) ? () => handleCut(delegate) : null, handleCopy: canCopy(delegate) ? () => handleCopy(delegate) : null, handlePaste: canPaste(delegate) ? () => handlePaste(delegate) : null, handleSelectAll: canSelectAll(delegate) ? () => handleSelectAll(delegate) : null, ); } /// Builder for material-style text selection handles. @override Widget buildHandle(BuildContext context, TextSelectionHandleType type, double textHeight, [VoidCallback? onTap]) { 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, ), child: GestureDetector( onTap: onTap, behavior: HitTestBehavior.translucent, ), ), ); // [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 Transform.rotate( angle: math.pi / 2.0, child: handle, ); case TextSelectionHandleType.right: // points up-left return handle; case TextSelectionHandleType.collapsed: // points up return Transform.rotate( angle: math.pi / 4.0, child: handle, ); } } /// 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; case TextSelectionHandleType.collapsed: return const Offset(_kHandleSize / 2, -4); } } @Deprecated( 'Use `contextMenuBuilder` instead. ' 'This feature was deprecated after v3.3.0-0.5.pre.', ) @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({ required this.clipboardStatus, required this.delegate, required this.endpoints, required this.globalEditableRegion, required this.handleCut, required this.handleCopy, required this.handlePaste, required this.handleSelectAll, required this.selectionMidpoint, required this.textLineHeight, }); final ValueListenable<ClipboardStatus>? clipboardStatus; final TextSelectionDelegate delegate; final List<TextSelectionPoint> endpoints; final Rect globalEditableRegion; final VoidCallback? handleCut; final VoidCallback? handleCopy; final VoidCallback? handlePaste; final VoidCallback? handleSelectAll; final Offset selectionMidpoint; final double textLineHeight; @override _TextSelectionControlsToolbarState createState() => _TextSelectionControlsToolbarState(); } class _TextSelectionControlsToolbarState extends State<_TextSelectionControlsToolbar> with TickerProviderStateMixin { void _onChangedClipboardStatus() { setState(() { // Inform the widget that the value of clipboardStatus has changed. }); } @override void initState() { super.initState(); widget.clipboardStatus?.addListener(_onChangedClipboardStatus); } @override void didUpdateWidget(_TextSelectionControlsToolbar oldWidget) { super.didUpdateWidget(oldWidget); if (widget.clipboardStatus != oldWidget.clipboardStatus) { widget.clipboardStatus?.addListener(_onChangedClipboardStatus); oldWidget.clipboardStatus?.removeListener(_onChangedClipboardStatus); } } @override void dispose() { widget.clipboardStatus?.removeListener(_onChangedClipboardStatus); super.dispose(); } @override Widget build(BuildContext context) { // 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. if (widget.handlePaste != null && widget.clipboardStatus?.value == ClipboardStatus.unknown) { return const SizedBox.shrink(); } // 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]; final double topAmountInEditableRegion = startTextSelectionPoint.point.dy - widget.textLineHeight; final double anchorTop = math.max(topAmountInEditableRegion, 0) + widget.globalEditableRegion.top - _kToolbarContentDistance; final Offset anchorAbove = Offset( widget.globalEditableRegion.left + widget.selectionMidpoint.dx, anchorTop, ); 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)); final MaterialLocalizations localizations = MaterialLocalizations.of(context); final List<_TextSelectionToolbarItemData> itemDatas = <_TextSelectionToolbarItemData>[ if (widget.handleCut != null) _TextSelectionToolbarItemData( label: localizations.cutButtonLabel, onPressed: widget.handleCut!, ), if (widget.handleCopy != null) _TextSelectionToolbarItemData( label: localizations.copyButtonLabel, onPressed: widget.handleCopy!, ), if (widget.handlePaste != null && widget.clipboardStatus?.value == ClipboardStatus.pasteable) _TextSelectionToolbarItemData( label: localizations.pasteButtonLabel, onPressed: widget.handlePaste!, ), if (widget.handleSelectAll != null) _TextSelectionToolbarItemData( label: localizations.selectAllButtonLabel, onPressed: widget.handleSelectAll!, ), ]; // If there is no option available, build an empty widget. if (itemDatas.isEmpty) { return const SizedBox.shrink(); } 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), ); }).toList(), ); } } /// Draws a single text selection handle which points up and to the left. class _TextSelectionHandlePainter extends CustomPainter { _TextSelectionHandlePainter({ required this.color }); final Color color; @override void paint(Canvas canvas, Size size) { final Paint paint = Paint()..color = color; final double radius = size.width/2.0; 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); } @override bool shouldRepaint(_TextSelectionHandlePainter oldPainter) { return color != oldPainter.color; } } // TODO(justinmc): Deprecate this after TextSelectionControls.buildToolbar is // deleted, when users should migrate back to materialTextSelectionControls. // See https://github.com/flutter/flutter/pull/124262 /// Text selection handle controls that follow the Material Design specification. final TextSelectionControls materialTextSelectionHandleControls = MaterialTextSelectionHandleControls(); /// Text selection controls that follow the Material Design specification. final TextSelectionControls materialTextSelectionControls = MaterialTextSelectionControls();