| // 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/gestures.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), |
| ); |
| |
| /// Desktop Cupertino styled text selection controls. |
| /// |
| /// The [cupertinoDesktopTextSelectionControls] global variable has a |
| /// suitable instance of this class. |
| 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; |
| } |
| |
| @override |
| void handleSelectAll(TextSelectionDelegate delegate) { |
| super.handleSelectAll(delegate); |
| delegate.hideToolbar(); |
| } |
| } |
| |
| /// 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({ |
| 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 |
| _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.shrink(); |
| } |
| |
| assert(debugCheckHasMediaQuery(context)); |
| final MediaQueryData mediaQuery = MediaQuery.of(context); |
| |
| final Offset midpointAnchor = Offset( |
| clampDouble(widget.selectionMidpoint.dx - widget.globalEditableRegion.left, |
| 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.shrink(); |
| } |
| |
| 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({ |
| required this.anchor, |
| required this.children, |
| }) : assert(children.length > 0); |
| |
| /// 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({ |
| required this.onPressed, |
| required this.child, |
| }); |
| |
| /// Create an instance of [CupertinoDesktopTextSelectionToolbarButton] whose child is |
| /// a [Text] widget styled like the default Mac context menu button. |
| _CupertinoDesktopTextSelectionToolbarButton.text({ |
| 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), |
| ), |
| ); |
| |
| /// {@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, |
| ), |
| ), |
| ); |
| } |
| } |