// 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'; import 'package:flutter/gestures.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'button.dart'; import 'colors.dart'; import 'localizations.dart'; import 'theme.dart'; // Minimal padding from all edges of the selection toolbar to all edges of the // screen. const double _kToolbarScreenPadding = 8.0; // These values were measured from a screenshot of TextEdit on MacOS 10.15.7 on // a Macbook Pro. const double _kToolbarWidth = 222.0; const Radius _kToolbarBorderRadius = Radius.circular(4.0); // These values were measured from a screenshot of TextEdit on MacOS 10.16 on a // Macbook Pro. const CupertinoDynamicColor _kToolbarBorderColor = CupertinoDynamicColor.withBrightness( color: Color(0xFFBBBBBB), darkColor: Color(0xFF505152), ); const CupertinoDynamicColor _kToolbarBackgroundColor = CupertinoDynamicColor.withBrightness( color: Color(0xffECE8E6), darkColor: Color(0xff302928), ); class _CupertinoDesktopTextSelectionControls extends TextSelectionControls { /// Desktop has no text selection handles. @override Size getHandleSize(double textLineHeight) { return Size.zero; } /// Builder for the Mac-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 _CupertinoDesktopTextSelectionControlsToolbar( 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; } } /// Text selection controls that follows Mac design conventions. final TextSelectionControls cupertinoDesktopTextSelectionControls = _CupertinoDesktopTextSelectionControls(); // Generates the child that's passed into CupertinoDesktopTextSelectionToolbar. class _CupertinoDesktopTextSelectionControlsToolbar extends StatefulWidget { const _CupertinoDesktopTextSelectionControlsToolbar({ 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 _CupertinoDesktopTextSelectionControlsToolbarState createState() => _CupertinoDesktopTextSelectionControlsToolbarState(); } class _CupertinoDesktopTextSelectionControlsToolbarState extends State<_CupertinoDesktopTextSelectionControlsToolbar> { 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(_CupertinoDesktopTextSelectionControlsToolbar oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.clipboardStatus != widget.clipboardStatus) { oldWidget.clipboardStatus?.removeListener(_onChangedClipboardStatus); widget.clipboardStatus?.addListener(_onChangedClipboardStatus); } } @override void dispose() { super.dispose(); widget.clipboardStatus?.removeListener(_onChangedClipboardStatus); } @override Widget build(BuildContext 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(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, ); final List<Widget> items = <Widget>[]; final CupertinoLocalizations localizations = CupertinoLocalizations.of(context); final Widget onePhysicalPixelVerticalDivider = SizedBox(width: 1.0 / MediaQuery.of(context).devicePixelRatio); void addToolbarButton( String text, VoidCallback onPressed, ) { if (items.isNotEmpty) { items.add(onePhysicalPixelVerticalDivider); } items.add(_CupertinoDesktopTextSelectionToolbarButton.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(width: 0.0, height: 0.0); } return _CupertinoDesktopTextSelectionToolbar( anchor: widget.lastSecondaryTapDownPosition ?? midpointAnchor, children: items, ); } } /// A Mac-style 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: /// /// * [TextSelectionControls.buildToolbar], where this is used by default to /// build a Mac-style toolbar. /// * [TextSelectionToolbar], which is similar, but builds an Android-style /// toolbar. class _CupertinoDesktopTextSelectionToolbar extends StatelessWidget { /// Creates an instance of CupertinoTextSelectionToolbar. const _CupertinoDesktopTextSelectionToolbar({ Key? key, required this.anchor, required this.children, }) : 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: /// * [CupertinoDesktopTextSelectionToolbarButton], which builds a default /// Mac-style text selection toolbar text button. final List<Widget> children; // Builds a toolbar just like the default Mac toolbar, with the right color // background, padding, and rounded corners. static Widget _defaultToolbarBuilder(BuildContext context, Widget child) { return Container( width: _kToolbarWidth, decoration: BoxDecoration( color: _kToolbarBackgroundColor.resolveFrom(context), border: Border.all( color: _kToolbarBorderColor.resolveFrom(context), ), borderRadius: const BorderRadius.all(_kToolbarBorderRadius), ), child: Padding( padding: const EdgeInsets.symmetric( // This value was measured from a screenshot of TextEdit on MacOS // 10.15.7 on a Macbook Pro. vertical: 3.0, ), 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: _defaultToolbarBuilder(context, Column( mainAxisSize: MainAxisSize.min, children: children, )), ), ); } } // These values were measured from a screenshot of TextEdit on MacOS 10.15.7 on // a Macbook Pro. const TextStyle _kToolbarButtonFontStyle = TextStyle( inherit: false, fontSize: 14.0, letterSpacing: -0.15, fontWeight: FontWeight.w400, ); // This value was measured from a screenshot of TextEdit on MacOS 10.15.7 on a // Macbook Pro. const EdgeInsets _kToolbarButtonPadding = EdgeInsets.fromLTRB( 20.0, 0.0, 20.0, 3.0, ); /// A button in the style of the Mac context menu buttons. class _CupertinoDesktopTextSelectionToolbarButton extends StatefulWidget { /// Creates an instance of CupertinoDesktopTextSelectionToolbarButton. const _CupertinoDesktopTextSelectionToolbarButton({ Key? key, required this.onPressed, required this.child, }) : super(key: key); /// Create an instance of [CupertinoDesktopTextSelectionToolbarButton] whose child is /// a [Text] widget styled like the default Mac context menu button. _CupertinoDesktopTextSelectionToolbarButton.text({ Key? key, required BuildContext context, required this.onPressed, required String text, }) : child = Text( text, overflow: TextOverflow.ellipsis, style: _kToolbarButtonFontStyle.copyWith( color: const CupertinoDynamicColor.withBrightness( color: CupertinoColors.black, darkColor: CupertinoColors.white, ).resolveFrom(context), ), ), super(key: key); /// {@macro flutter.cupertino.CupertinoTextSelectionToolbarButton.onPressed} final VoidCallback onPressed; /// {@macro flutter.cupertino.CupertinoTextSelectionToolbarButton.child} final Widget child; @override _CupertinoDesktopTextSelectionToolbarButtonState createState() => _CupertinoDesktopTextSelectionToolbarButtonState(); } class _CupertinoDesktopTextSelectionToolbarButtonState extends State<_CupertinoDesktopTextSelectionToolbarButton> { bool _isHovered = false; void _onEnter(PointerEnterEvent event) { setState(() { _isHovered = true; }); } void _onExit(PointerExitEvent event) { setState(() { _isHovered = false; }); } @override Widget build(BuildContext context) { return SizedBox( width: double.infinity, child: MouseRegion( onEnter: _onEnter, onExit: _onExit, child: CupertinoButton( alignment: Alignment.centerLeft, borderRadius: null, color: _isHovered ? CupertinoTheme.of(context).primaryColor : null, minSize: 0.0, onPressed: widget.onPressed, padding: _kToolbarButtonPadding, pressedOpacity: 0.7, child: widget.child, ), ), ); } }