| // 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 'package:flutter/foundation.dart' show clampDouble; |
| import 'package:flutter/widgets.dart'; |
| |
| import 'debug.dart'; |
| import 'desktop_text_selection_toolbar.dart'; |
| import 'desktop_text_selection_toolbar_button.dart'; |
| import 'material_localizations.dart'; |
| |
| /// Desktop Material styled text selection handle controls. |
| /// |
| /// Specifically does not manage the toolbar, which is left to |
| /// [EditableText.contextMenuBuilder]. |
| class _DesktopTextSelectionHandleControls extends DesktopTextSelectionControls with TextSelectionHandleControls { |
| } |
| |
| /// Desktop Material styled text selection controls. |
| /// |
| /// The [desktopTextSelectionControls] global variable has a |
| /// suitable instance of this class. |
| class DesktopTextSelectionControls extends TextSelectionControls { |
| /// Desktop has no text selection handles. |
| @override |
| Size getHandleSize(double textLineHeight) { |
| return Size.zero; |
| } |
| |
| /// Builder for the Material-style desktop 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, |
| ClipboardStatusNotifier? clipboardStatus, |
| Offset? lastSecondaryTapDownPosition, |
| ) { |
| return _DesktopTextSelectionControlsToolbar( |
| clipboardStatus: clipboardStatus, |
| endpoints: endpoints, |
| globalEditableRegion: globalEditableRegion, |
| handleCut: canCut(delegate) ? () => handleCut(delegate) : null, |
| handleCopy: canCopy(delegate) ? () => handleCopy(delegate) : null, |
| handlePaste: canPaste(delegate) ? () => handlePaste(delegate) : null, |
| handleSelectAll: canSelectAll(delegate) ? () => handleSelectAll(delegate) : null, |
| selectionMidpoint: selectionMidpoint, |
| lastSecondaryTapDownPosition: lastSecondaryTapDownPosition, |
| textLineHeight: textLineHeight, |
| ); |
| } |
| |
| /// Builds the text selection handles, but desktop has none. |
| @override |
| Widget buildHandle(BuildContext context, TextSelectionHandleType type, double textLineHeight, [VoidCallback? onTap]) { |
| return const SizedBox.shrink(); |
| } |
| |
| /// Gets the position for the text selection handles, but desktop has none. |
| @override |
| Offset getHandleAnchor(TextSelectionHandleType type, double textLineHeight) { |
| return Offset.zero; |
| } |
| |
| @Deprecated( |
| 'Use `contextMenuBuilder` instead. ' |
| 'This feature was deprecated after v3.3.0-0.5.pre.', |
| ) |
| @override |
| bool canSelectAll(TextSelectionDelegate delegate) { |
| // Allow SelectAll when selection is not collapsed, unless everything has |
| // already been selected. Same behavior as Android. |
| final TextEditingValue value = delegate.textEditingValue; |
| return delegate.selectAllEnabled && |
| value.text.isNotEmpty && |
| !(value.selection.start == 0 && value.selection.end == value.text.length); |
| } |
| |
| @Deprecated( |
| 'Use `contextMenuBuilder` instead. ' |
| 'This feature was deprecated after v3.3.0-0.5.pre.', |
| ) |
| @override |
| void handleSelectAll(TextSelectionDelegate delegate) { |
| super.handleSelectAll(delegate); |
| delegate.hideToolbar(); |
| } |
| } |
| |
| /// Desktop text selection handle controls that loosely follow Material design |
| /// conventions. |
| @Deprecated( |
| 'Use `desktopTextSelectionControls` instead. ' |
| 'This feature was deprecated after v3.3.0-0.5.pre.', |
| ) |
| final TextSelectionControls desktopTextSelectionHandleControls = |
| _DesktopTextSelectionHandleControls(); |
| |
| /// Desktop text selection controls that loosely follow Material design |
| /// conventions. |
| final TextSelectionControls desktopTextSelectionControls = |
| DesktopTextSelectionControls(); |
| |
| // Generates the child that's passed into DesktopTextSelectionToolbar. |
| class _DesktopTextSelectionControlsToolbar extends StatefulWidget { |
| const _DesktopTextSelectionControlsToolbar({ |
| required this.clipboardStatus, |
| required this.endpoints, |
| required this.globalEditableRegion, |
| required this.handleCopy, |
| required this.handleCut, |
| required this.handlePaste, |
| required this.handleSelectAll, |
| required this.selectionMidpoint, |
| required this.textLineHeight, |
| required this.lastSecondaryTapDownPosition, |
| }); |
| |
| final ClipboardStatusNotifier? clipboardStatus; |
| final List<TextSelectionPoint> endpoints; |
| final Rect globalEditableRegion; |
| final VoidCallback? handleCopy; |
| final VoidCallback? handleCut; |
| final VoidCallback? handlePaste; |
| final VoidCallback? handleSelectAll; |
| final Offset? lastSecondaryTapDownPosition; |
| final Offset selectionMidpoint; |
| final double textLineHeight; |
| |
| @override |
| _DesktopTextSelectionControlsToolbarState createState() => _DesktopTextSelectionControlsToolbarState(); |
| } |
| |
| class _DesktopTextSelectionControlsToolbarState extends State<_DesktopTextSelectionControlsToolbar> { |
| 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(_DesktopTextSelectionControlsToolbar oldWidget) { |
| super.didUpdateWidget(oldWidget); |
| if (oldWidget.clipboardStatus != widget.clipboardStatus) { |
| oldWidget.clipboardStatus?.removeListener(_onChangedClipboardStatus); |
| widget.clipboardStatus?.addListener(_onChangedClipboardStatus); |
| } |
| } |
| |
| @override |
| void dispose() { |
| widget.clipboardStatus?.removeListener(_onChangedClipboardStatus); |
| super.dispose(); |
| } |
| |
| @override |
| Widget build(BuildContext context) { |
| assert(debugCheckHasMaterialLocalizations(context)); |
| assert(debugCheckHasMediaQuery(context)); |
| |
| // Don't render the menu until the state of the clipboard is known. |
| if (widget.handlePaste != null && widget.clipboardStatus?.value == ClipboardStatus.unknown) { |
| return const SizedBox.shrink(); |
| } |
| |
| final EdgeInsets mediaQueryPadding = MediaQuery.paddingOf(context); |
| final Offset midpointAnchor = Offset( |
| clampDouble(widget.selectionMidpoint.dx - widget.globalEditableRegion.left, |
| mediaQueryPadding.left, |
| MediaQuery.sizeOf(context).width - mediaQueryPadding.right, |
| ), |
| widget.selectionMidpoint.dy - widget.globalEditableRegion.top, |
| ); |
| |
| final MaterialLocalizations localizations = MaterialLocalizations.of(context); |
| final List<Widget> items = <Widget>[]; |
| |
| void addToolbarButton( |
| String text, |
| VoidCallback onPressed, |
| ) { |
| items.add(DesktopTextSelectionToolbarButton.text( |
| context: context, |
| onPressed: onPressed, |
| text: text, |
| )); |
| } |
| |
| if (widget.handleCut != null) { |
| addToolbarButton(localizations.cutButtonLabel, widget.handleCut!); |
| } |
| if (widget.handleCopy != null) { |
| addToolbarButton(localizations.copyButtonLabel, widget.handleCopy!); |
| } |
| if (widget.handlePaste != null |
| && widget.clipboardStatus?.value == ClipboardStatus.pasteable) { |
| addToolbarButton(localizations.pasteButtonLabel, widget.handlePaste!); |
| } |
| if (widget.handleSelectAll != null) { |
| addToolbarButton(localizations.selectAllButtonLabel, widget.handleSelectAll!); |
| } |
| |
| // If there is no option available, build an empty widget. |
| if (items.isEmpty) { |
| return const SizedBox.shrink(); |
| } |
| |
| return DesktopTextSelectionToolbar( |
| anchor: widget.lastSecondaryTapDownPosition ?? midpointAnchor, |
| children: items, |
| ); |
| } |
| } |