// 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/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'colors.dart'; import 'constants.dart'; import 'debug.dart'; import 'material.dart'; import 'material_localizations.dart'; import 'text_button.dart'; import 'text_selection_toolbar.dart'; import 'theme.dart'; const double _kToolbarScreenPadding = 8.0; const double _kToolbarWidth = 222.0; 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. @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, clipboardStatus) : 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; } @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); } } /// Text selection controls that loosely follows Material design conventions. final TextSelectionControls desktopTextSelectionControls = _DesktopTextSelectionControls(); // Generates the child that's passed into DesktopTextSelectionToolbar. class _DesktopTextSelectionControlsToolbar extends StatefulWidget { const _DesktopTextSelectionControlsToolbar({ Key? key, 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, }) : super(key: key); 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> { ClipboardStatusNotifier? _clipboardStatus; void _onChangedClipboardStatus() { setState(() { // Inform the widget that the value of clipboardStatus has changed. }); } @override void initState() { super.initState(); if (widget.handlePaste != null) { _clipboardStatus = widget.clipboardStatus ?? ClipboardStatusNotifier(); _clipboardStatus!.addListener(_onChangedClipboardStatus); _clipboardStatus!.update(); } } @override void didUpdateWidget(_DesktopTextSelectionControlsToolbar oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.clipboardStatus != widget.clipboardStatus) { if (_clipboardStatus != null) { _clipboardStatus!.removeListener(_onChangedClipboardStatus); _clipboardStatus!.dispose(); } _clipboardStatus = widget.clipboardStatus ?? ClipboardStatusNotifier(); _clipboardStatus!.addListener(_onChangedClipboardStatus); if (widget.handlePaste != null) { _clipboardStatus!.update(); } } } @override void dispose() { super.dispose(); // When used in an Overlay, this can be disposed after its creator has // already disposed _clipboardStatus. if (_clipboardStatus != null && !_clipboardStatus!.disposed) { _clipboardStatus!.removeListener(_onChangedClipboardStatus); if (widget.clipboardStatus == null) { _clipboardStatus!.dispose(); } } } @override Widget build(BuildContext context) { // Don't render the menu until the state of the clipboard is known. if (widget.handlePaste != null && _clipboardStatus!.value == ClipboardStatus.unknown) { return const SizedBox(width: 0.0, height: 0.0); } assert(debugCheckHasMediaQuery(context)); final MediaQueryData mediaQuery = MediaQuery.of(context); final Offset midpointAnchor = Offset( (widget.selectionMidpoint.dx - widget.globalEditableRegion.left).clamp( mediaQuery.padding.left, mediaQuery.size.width - mediaQuery.padding.right, ), widget.selectionMidpoint.dy - widget.globalEditableRegion.top, ); assert(debugCheckHasMaterialLocalizations(context)); 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 && _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(width: 0.0, height: 0.0); } return _DesktopTextSelectionToolbar( anchor: widget.lastSecondaryTapDownPosition ?? midpointAnchor, children: items, ); } } /// A Material-style desktop text selection toolbar. /// /// Typically displays buttons for text manipulation, e.g. copying and pasting /// text. /// /// Tries to position itself as closely as possible to [anchor] while remaining /// fully on-screen. /// /// See also: /// /// * [_DesktopTextSelectionControls.buildToolbar], where this is used by /// default to build a Material-style desktop toolbar. /// * [TextSelectionToolbar], which is similar, but builds an Android-style /// toolbar. class _DesktopTextSelectionToolbar extends StatelessWidget { /// Creates an instance of _DesktopTextSelectionToolbar. const _DesktopTextSelectionToolbar({ Key? key, required this.anchor, required this.children, this.toolbarBuilder = _defaultToolbarBuilder, }) : assert(children.length > 0), super(key: key); /// The point at which the toolbar will attempt to position itself as closely /// as possible. final Offset anchor; /// {@macro flutter.material.TextSelectionToolbar.children} /// /// See also: /// * [DesktopTextSelectionToolbarButton], which builds a default /// Material-style desktop text selection toolbar text button. final List<Widget> children; /// {@macro flutter.material.TextSelectionToolbar.toolbarBuilder} /// /// The given anchor and isAbove can be used to position an arrow, as in the /// default toolbar. final ToolbarBuilder toolbarBuilder; // Builds a desktop toolbar in the Material style. static Widget _defaultToolbarBuilder(BuildContext context, Widget child) { return SizedBox( width: _kToolbarWidth, child: Material( borderRadius: const BorderRadius.all(Radius.circular(7.0)), clipBehavior: Clip.antiAlias, elevation: 1.0, type: MaterialType.card, child: child, ), ); } @override Widget build(BuildContext context) { assert(debugCheckHasMediaQuery(context)); final MediaQueryData mediaQuery = MediaQuery.of(context); final double paddingAbove = mediaQuery.padding.top + _kToolbarScreenPadding; final Offset localAdjustment = Offset(_kToolbarScreenPadding, paddingAbove); return Padding( padding: EdgeInsets.fromLTRB( _kToolbarScreenPadding, paddingAbove, _kToolbarScreenPadding, _kToolbarScreenPadding, ), child: CustomSingleChildLayout( delegate: DesktopTextSelectionToolbarLayoutDelegate( anchor: anchor - localAdjustment, ), child: toolbarBuilder(context, Column( mainAxisSize: MainAxisSize.min, children: children, )), ), ); } } const TextStyle _kToolbarButtonFontStyle = TextStyle( inherit: false, fontSize: 14.0, letterSpacing: -0.15, fontWeight: FontWeight.w400, ); const EdgeInsets _kToolbarButtonPadding = EdgeInsets.fromLTRB( 20.0, 0.0, 20.0, 3.0, ); /// A [TextButton] for the Material desktop text selection toolbar. class _DesktopTextSelectionToolbarButton extends StatelessWidget { /// Creates an instance of DesktopTextSelectionToolbarButton. const _DesktopTextSelectionToolbarButton({ Key? key, required this.onPressed, required this.child, }) : super(key: key); /// Create an instance of [_DesktopTextSelectionToolbarButton] whose child is /// a [Text] widget in the style of the Material text selection toolbar. _DesktopTextSelectionToolbarButton.text({ Key? key, required BuildContext context, required this.onPressed, required String text, }) : child = Text( text, overflow: TextOverflow.ellipsis, style: _kToolbarButtonFontStyle.copyWith( color: Theme.of(context).colorScheme.brightness == Brightness.dark ? Colors.white : Colors.black87, ), ), super(key: key); /// {@macro flutter.material.TextSelectionToolbarTextButton.onPressed} final VoidCallback onPressed; /// {@macro flutter.material.TextSelectionToolbarTextButton.child} final Widget child; @override Widget build(BuildContext context) { // TODO(hansmuller): Should be colorScheme.onSurface final ThemeData theme = Theme.of(context); final bool isDark = theme.colorScheme.brightness == Brightness.dark; final Color primary = isDark ? Colors.white : Colors.black87; return SizedBox( width: double.infinity, child: TextButton( style: TextButton.styleFrom( alignment: Alignment.centerLeft, primary: primary, shape: const RoundedRectangleBorder(), minimumSize: const Size(kMinInteractiveDimension, 36.0), padding: _kToolbarButtonPadding, ), onPressed: onPressed, child: child, ), ); } }