290 lines
10 KiB
Dart
290 lines
10 KiB
Dart
// 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/rendering.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 controls.
|
|
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.
|
|
@override
|
|
Widget buildToolbar(
|
|
BuildContext context,
|
|
Rect globalEditableRegion,
|
|
double textLineHeight,
|
|
Offset selectionMidpoint,
|
|
List<TextSelectionPoint> endpoints,
|
|
TextSelectionDelegate delegate,
|
|
ClipboardStatusNotifier? 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);
|
|
}
|
|
}
|
|
|
|
@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({
|
|
Key? key,
|
|
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,
|
|
}) : super(key: key);
|
|
|
|
final ClipboardStatusNotifier? 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() {
|
|
super.dispose();
|
|
widget.clipboardStatus?.removeListener(_onChangedClipboardStatus);
|
|
}
|
|
|
|
@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 Offset anchorAbove = Offset(
|
|
widget.globalEditableRegion.left + widget.selectionMidpoint.dx,
|
|
widget.globalEditableRegion.top + startTextSelectionPoint.point.dy - widget.textLineHeight - _kToolbarContentDistance,
|
|
);
|
|
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(width: 0.0, height: 0.0);
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// Text selection controls that follow the Material Design specification.
|
|
final TextSelectionControls materialTextSelectionControls = MaterialTextSelectionControls();
|