From 3c761af4f4f5930d6aa662aed3b4806bb52e3ae3 Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Fri, 31 Mar 2023 00:48:49 +0700 Subject: [PATCH] TF-1545: Update pick color dialog in rich text (cherry picked from commit e70c9871c3e47ac2d0f2ed47265cae398665d984) --- .../extensions/color_extension.dart | 53 ++ .../dialog/color_picker_dialog_builder.dart | 136 +++-- .../views/pick_color/color_code_field.dart | 356 ++++++++++++ .../color_picker_action_buttons.dart | 427 +++++++++++++++ .../color_picker_copy_past_behaivor.dart | 511 ++++++++++++++++++ .../pick_color/color_picker_extensions.dart | 214 ++++++++ .../views/pick_color/dry_intrinsic.dart | 77 +++ core/pubspec.lock | 18 +- core/pubspec.yaml | 2 - .../controller/base_rich_text_controller.dart | 2 +- 10 files changed, 1739 insertions(+), 57 deletions(-) create mode 100644 core/lib/presentation/views/pick_color/color_code_field.dart create mode 100644 core/lib/presentation/views/pick_color/color_picker_action_buttons.dart create mode 100644 core/lib/presentation/views/pick_color/color_picker_copy_past_behaivor.dart create mode 100644 core/lib/presentation/views/pick_color/color_picker_extensions.dart create mode 100644 core/lib/presentation/views/pick_color/dry_intrinsic.dart diff --git a/core/lib/presentation/extensions/color_extension.dart b/core/lib/presentation/extensions/color_extension.dart index 93fcec29a..e2d6739d2 100644 --- a/core/lib/presentation/extensions/color_extension.dart +++ b/core/lib/presentation/extensions/color_extension.dart @@ -192,6 +192,39 @@ extension AppColor on Color { .toRadixString(16) .padLeft(6, '0') .toUpperCase()}'; + + static List get listColorsPicker { + return [ + ...Colors.grey.listShadeColors, + ...listMaterialColors.map((color) => color.shade900).toList(), + ...listMaterialColors.map((color) => color.shade800).toList(), + ...listMaterialColors.map((color) => color.shade700).toList(), + ...listMaterialColors.map((color) => color.shade600).toList(), + ...listMaterialColors.map((color) => color.shade500).toList(), + ...listMaterialColors.map((color) => color.shade400).toList(), + ...listMaterialColors.map((color) => color.shade300).toList(), + ...listMaterialColors.map((color) => color.shade200).toList(), + ...listMaterialColors.map((color) => color.shade100).toList(), + ...listMaterialColors.map((color) => color.shade50).toList(), + ]; + } + + static List get listMaterialColors { + return [ + Colors.cyan, + Colors.blue, + Colors.deepPurple, + Colors.purple, + Colors.pink, + Colors.red, + Colors.deepOrange, + Colors.orange, + Colors.amber, + Colors.yellow, + Colors.lime, + Colors.green + ]; + } } extension ColorNullableExtension on Color? { @@ -203,3 +236,23 @@ extension ColorNullableExtension on Color? { } } } + +extension MaterialColorExtension on MaterialColor { + + List get listShadeColors { + return [ + const Color(0xFFFFFFFF), + const Color(0xFFFCFCFC), + shade50, + shade100, + shade200, + shade300, + shade400, + shade500, + shade600, + shade700, + shade800, + shade900 + ]; + } +} \ No newline at end of file diff --git a/core/lib/presentation/views/dialog/color_picker_dialog_builder.dart b/core/lib/presentation/views/dialog/color_picker_dialog_builder.dart index 2b2e17899..e9b287b41 100644 --- a/core/lib/presentation/views/dialog/color_picker_dialog_builder.dart +++ b/core/lib/presentation/views/dialog/color_picker_dialog_builder.dart @@ -1,31 +1,35 @@ import 'package:core/presentation/extensions/color_extension.dart'; import 'package:core/presentation/views/button/icon_button_web.dart'; -import 'package:flex_color_picker/flex_color_picker.dart'; +import 'package:core/presentation/views/pick_color/color_code_field.dart'; +import 'package:core/presentation/views/pick_color/color_picker_action_buttons.dart'; +import 'package:core/presentation/views/pick_color/color_picker_copy_past_behaivor.dart'; import 'package:flutter/material.dart'; import 'package:pointer_interceptor/pointer_interceptor.dart'; typedef SelectColorActionCallback = Function(Color? colorSelected); class ColorPickerDialogBuilder { - final SelectColorActionCallback? setColorActionCallback; final VoidCallback? cancelActionCallback; final VoidCallback? resetToDefaultActionCallback; final BuildContext _context; final Color defaultColor; - final Color _currentColor; + final ValueNotifier _currentColor; final String? title; final String? textActionSetColor; final String? textActionCancel; final String? textActionResetDefault; + final Function(Color)? onSelected; - Color? _colorSelected; + bool _shouldUpdate = false; + Color _colorCode = Colors.black; ColorPickerDialogBuilder( this._context, this._currentColor, { + this.onSelected, this.title, this.textActionSetColor, this.textActionCancel, @@ -35,22 +39,23 @@ class ColorPickerDialogBuilder { this.cancelActionCallback, this.resetToDefaultActionCallback } - ) : _colorSelected = _currentColor; + ); Future show() async { await showDialog(context: _context, builder: (BuildContext context) { return PointerInterceptor( child: AlertDialog( - title: Text(title ?? '', - textAlign: TextAlign.center, - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 20, - color: Colors.black)), - titleTextStyle: const TextStyle( + title: Text( + title ?? '', + textAlign: TextAlign.center, + style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 20, - color: Colors.black), + color: Colors.black)), + titleTextStyle: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 20, + color: Colors.black), titlePadding: const EdgeInsets.symmetric(vertical: 16, horizontal: 16), contentPadding: const EdgeInsets.symmetric(vertical: 0, horizontal: 16), actionsPadding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), @@ -59,27 +64,55 @@ class ColorPickerDialogBuilder { shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), scrollable: true, elevation: 10, - content: ColorPicker( - color: _currentColor, - onColorChanged: (color) => _colorSelected = color, - width: 40, - height: 40, - spacing: 0, - runSpacing: 0, - borderRadius: 0, - wheelDiameter: 165, - enableOpacity: false, - showColorCode: true, - colorCodeHasColor: true, - pickersEnabled: const { - ColorPickerType.wheel: true, + content: ValueListenableBuilder( + valueListenable: _currentColor, + builder: (context, _, __) { + return Column( + children: [ + Container( + width: 600, + decoration: const BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(4)), + color: Colors.white + ), + child: ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(4)), + child: Wrap(children: AppColor.listColorsPicker + .map((color) => _itemColorWidget(context, color)) + .toList(), + ), + ), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 16), + child: ColorCodeField( + color: _currentColor.value, + colorCodeHasColor: true, + shouldUpdate: _shouldUpdate, + onColorChanged: (Color color) { + if (AppColor.listColorsPicker.any((element) => element.value == color.value)) { + _shouldUpdate = true; + _currentColor.value = color; + } else { + _shouldUpdate = false; + _currentColor.value = Colors.black; + _colorCode = color; + } + }, + onEditFocused: (bool editInFocus) { + _shouldUpdate = editInFocus ? true : false; + }, + copyPasteBehavior: const ColorPickerCopyPasteBehavior( + parseShortHexCode: true, + ), + toolIcons: const ColorPickerActionButtons( + dialogActionButtons: true, + ), + ), + ), + ], + ); }, - copyPasteBehavior: const ColorPickerCopyPasteBehavior( - parseShortHexCode: true, - ), - actionButtons: const ColorPickerActionButtons( - dialogActionButtons: true, - ), ), actions: [ buildButtonWrapText( @@ -108,13 +141,42 @@ class ColorPickerDialogBuilder { radius: 5, height: 30, textStyle: const TextStyle( - color: Colors.white, - fontSize: 16, - fontWeight: FontWeight.w500), - onTap: () => setColorActionCallback?.call(_colorSelected)) + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.w500), + onTap: () { + if (!_shouldUpdate) { + setColorActionCallback?.call(_colorCode); + } else { + setColorActionCallback?.call(_currentColor.value); + } + }) ], ), ); }); } + + Widget _itemColorWidget(BuildContext context, Color color) { + return Material( + color: Colors.transparent, + child: InkWell( + onTap: () { + _shouldUpdate = true; + _currentColor.value = color; + }, + child: Container( + decoration: BoxDecoration( + color: color, + border: Border.all( + color: _currentColor.value == color ? Colors.white : Colors.transparent, + width: 8, + ), + ), + width: 40, + height: 40, + ), + ), + ); + } } \ No newline at end of file diff --git a/core/lib/presentation/views/pick_color/color_code_field.dart b/core/lib/presentation/views/pick_color/color_code_field.dart new file mode 100644 index 000000000..4f8502ab3 --- /dev/null +++ b/core/lib/presentation/views/pick_color/color_code_field.dart @@ -0,0 +1,356 @@ +import 'package:core/presentation/views/pick_color/color_picker_action_buttons.dart'; +import 'package:core/presentation/views/pick_color/color_picker_copy_past_behaivor.dart'; +import 'package:core/presentation/views/pick_color/color_picker_extensions.dart'; +import 'package:core/presentation/views/pick_color/dry_intrinsic.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +/// Color code entry and display field used by the color picker. +class ColorCodeField extends StatefulWidget { + /// Default const constructor. + const ColorCodeField({ + Key? key, + required this.color, + this.readOnly = false, + required this.onColorChanged, + required this.onEditFocused, + this.textStyle, + this.prefixStyle, + this.colorCodeHasColor = false, + this.toolIcons = const ColorPickerActionButtons(), + this.copyPasteBehavior = const ColorPickerCopyPasteBehavior(), + this.enableTooltips = true, + this.shouldUpdate = false, + }) : super(key: key); + + /// Current color value for the field. + final Color color; + + /// Is in read only mode, we should not be able to select either. + final bool readOnly; + + /// Color code of the entered color string is returned back in this callback. + final ValueChanged onColorChanged; + + /// The Color code editing field has focus. + final ValueChanged onEditFocused; + + /// TextStyle of the color code display and edit field. + /// + /// Defaults to Theme.of(context).textTheme.bodyMedium; + final TextStyle? textStyle; + + /// The TextStyle of the prefix of the color code. + /// + /// Defaults to [textStyle], if not defined. + final TextStyle? prefixStyle; + + /// If true then the background of the color code entry field uses the current + /// selected color. + /// + /// This makes the color code entry field a larger current color indicator + /// area that changes color as the color value is changed. + /// The text color of the filed will adjust to for best contrast as will + /// the opacity indicator text. Enabling this feature will override any + /// color specified in [textStyle] and [prefixStyle] but + /// their styles will otherwise be kept as specified. + /// + /// Defaults to false. + final bool colorCodeHasColor; + + /// Defines icons for the color picker title bar and its actions. + /// + /// Defaults to ColorPickerToolIcons(). + final ColorPickerActionButtons toolIcons; + + /// Defines the color picker's copy and paste behavior. + /// + /// Defaults to ColorPickerPasteBehavior(). + final ColorPickerCopyPasteBehavior copyPasteBehavior; + + /// Controls if tooltips are shown or not + /// + /// Defaults to true. + final bool enableTooltips; + + /// Should a change on the color value update the field? + /// + /// If we are just editing text in the control it should not, we just send + /// the data out to update any widget using the [color]. + /// However, when we get a new color due to external action is should update. + /// This is similar to the same property on the wheel. + /// + /// Defaults to false. + final bool shouldUpdate; + + @override + // ignore: library_private_types_in_public_api + _ColorCodeFieldState createState() => _ColorCodeFieldState(); +} + +// Color code display and entry field. +class _ColorCodeFieldState extends State { + late TextEditingController textController; + late FocusNode textFocusNode; + late String colorHexCode; + late Color color; + + @override + void initState() { + super.initState(); + textController = TextEditingController(); + textFocusNode = FocusNode(); + color = widget.color; + textController.text = color.hex; + } + + @override + void dispose() { + textController.dispose(); + textFocusNode.dispose(); + super.dispose(); + } + + @override + void didUpdateWidget(covariant ColorCodeField oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.color != widget.color && widget.shouldUpdate) { + color = widget.color; + textController.text = color.hex; + } + } + + @override + Widget build(BuildContext context) { + // The tooltip for copying the color code via the icon button + String? copyTooltip; + + if (widget.enableTooltips) { + // Get current platform. + final TargetPlatform platform = Theme.of(context).platform; + // Get the Material localizations. + final MaterialLocalizations translate = MaterialLocalizations.of(context); + // If shortcut key enabled, make a shortcut platform aware info tooltip. + String copyKeyTooltip = ''; + if (widget.copyPasteBehavior.ctrlC) { + copyKeyTooltip = platformControlKey(platform, 'C'); + } + // Make the Copy tooltip. + copyTooltip = + (widget.copyPasteBehavior.copyTooltip ?? translate.copyButtonLabel) + + copyKeyTooltip; + } + + // Define opinionated styles for the color code display and entry field. + final bool isLight = Theme.of(context).brightness == Brightness.light; + final Color fieldBackground = widget.colorCodeHasColor + ? color + : isLight + ? Colors.black.withAlpha(11) + : Colors.white.withAlpha(33); + + final bool isLightBackground = + ThemeData.estimateBrightnessForColor(fieldBackground) == + Brightness.light; + final Color textColor = isLight + ? (isLightBackground || fieldBackground.opacity < 0.5) + ? Colors.black + : Colors.white + : (!isLightBackground || fieldBackground.opacity < 0.5) + ? Colors.white + : Colors.black; + + final Color fieldBorder = + isLight ? Colors.black.withAlpha(33) : Colors.white.withAlpha(55); + + // Set the default text style to bodyMedium if not given. + TextStyle effectiveStyle = widget.textStyle ?? + Theme.of(context).textTheme.bodyMedium ?? + const TextStyle(fontSize: 14); + + TextStyle effectivePrefixStyle = widget.prefixStyle ?? effectiveStyle; + + if (widget.colorCodeHasColor) { + effectiveStyle = effectiveStyle.copyWith(color: textColor); + effectivePrefixStyle = effectivePrefixStyle.copyWith(color: textColor); + } + + // Compute color code field size based on the used font size. Might not + // always be ideal, but with normal fonts and sizes they have been tested to + // work well enough visually and to always have room for "DDDDDD", which is + // usually the widest possible entry string. + final double fontSize = effectiveStyle.fontSize ?? 14.0; + final double iconSize = fontSize * 1.1; + final double borderRadius = fontSize * 1.2; + final double fieldWidth = fontSize * 10; + + return SizedBox( + width: fieldWidth, + // The custom DryIntrinsicWidth layout widget is used due to issue: + // https://github.com/flutter/flutter/issues/71687 + child: DryIntrinsicWidth( + child: Focus( + // Tell the parent when the text edit field has focus. + onFocusChange: widget.onEditFocused, + child: TextField( + enabled: true, + readOnly: widget.readOnly, + enableInteractiveSelection: !widget.readOnly, + controller: textController, + focusNode: textFocusNode, + maxLength: 6, + maxLengthEnforcement: MaxLengthEnforcement.enforced, + // Remove line that shows entered chars when maxLength is used. + buildCounter: (BuildContext context, + {required int currentLength, + int? maxLength, + required bool isFocused}) => + null, + style: effectiveStyle, + // Only affects the type of keyboard shown on devices, does not + // make the input uppercase. + textCapitalization: TextCapitalization.characters, + // These input formatters limits the input to only valid chars for + // a hex color code, and we also convert them to uppercase. + inputFormatters: [ + FilteringTextInputFormatter.allow(RegExp('[a-fA-F0-9]')), + _UpperCaseTextFormatter(), + ], + decoration: InputDecoration( + suffixIcon: widget.copyPasteBehavior.editFieldCopyButton + ? IconButton( + icon: Icon(widget.copyPasteBehavior.copyIcon), + padding: EdgeInsets.zero, + tooltip: copyTooltip, + iconSize: iconSize, + splashRadius: borderRadius, + color: effectiveStyle.color, + constraints: const BoxConstraints(), + onPressed: _setClipboard, + ) + : SizedBox(height: borderRadius * 2), + suffixIconConstraints: BoxConstraints( + minHeight: borderRadius * 2, + minWidth: borderRadius * 2, + ), + isDense: true, + contentPadding: EdgeInsetsDirectional.only(start: fontSize), + prefixText: _editColorPrefix, + prefixStyle: effectivePrefixStyle, + filled: true, + fillColor: fieldBackground, + border: OutlineInputBorder( + borderSide: BorderSide.none, + borderRadius: BorderRadius.circular(borderRadius), + ), + focusedBorder: OutlineInputBorder( + borderSide: BorderSide( + color: fieldBorder, + ), + borderRadius: BorderRadius.circular(borderRadius), + ), + enabledBorder: OutlineInputBorder( + borderSide: BorderSide( + color: fieldBorder, + ), + borderRadius: BorderRadius.circular(borderRadius), + ), + disabledBorder: OutlineInputBorder( + borderSide: BorderSide( + color: fieldBorder, + ), + borderRadius: BorderRadius.circular(borderRadius), + ), + errorBorder: OutlineInputBorder( + borderSide: BorderSide( + color: fieldBorder, + ), + borderRadius: BorderRadius.circular(borderRadius), + ), + focusedErrorBorder: OutlineInputBorder( + borderSide: BorderSide( + color: fieldBorder, + ), + borderRadius: BorderRadius.circular(borderRadius), + ), + ), + // + onChanged: (String textColor) { + setState(() { + color = textColor + .toColorShort(widget.copyPasteBehavior.parseShortHexCode) + .withOpacity(color.opacity); + }); + widget.onColorChanged(color); + }, + onEditingComplete: () { + setState(() { + color = textController.text + .toColorShort(widget.copyPasteBehavior.parseShortHexCode) + .withOpacity(color.opacity); + }); + textController.text = color.hex; + widget.onColorChanged(color); + textFocusNode.unfocus(); + }, + ), + ), + ), + ); + } + + // Set current selected color values as a String on the Clipboard in the + // currently configured format. + Future _setClipboard() async { + String colorString = '00000000'; + switch (widget.copyPasteBehavior.copyFormat) { + case ColorPickerCopyFormat.dartCode: + colorString = '0x${color.hexAlpha}'; + break; + case ColorPickerCopyFormat.hexRRGGBB: + colorString = color.hex; + break; + case ColorPickerCopyFormat.hexAARRGGBB: + colorString = color.hexAlpha; + break; + case ColorPickerCopyFormat.numHexRRGGBB: + colorString = '#${color.hex}'; + break; + case ColorPickerCopyFormat.numHexAARRGGBB: + colorString = '#${color.hexAlpha}'; + break; + } + final ClipboardData data = ClipboardData(text: colorString); + await Clipboard.setData(data); + } + + // Get the current selected color prefix format for the input field. + // The prefix in the input field matches the set copy format. + String get _editColorPrefix { + final String alphaValue = color.hexAlpha.substring(0, 2); + switch (widget.copyPasteBehavior.copyFormat) { + case ColorPickerCopyFormat.dartCode: + return '0x$alphaValue'; + case ColorPickerCopyFormat.hexRRGGBB: + return ' '; + case ColorPickerCopyFormat.hexAARRGGBB: + return ' $alphaValue'; + case ColorPickerCopyFormat.numHexRRGGBB: + return ' #'; + case ColorPickerCopyFormat.numHexAARRGGBB: + return ' #$alphaValue'; + } + } +} + +// This TextField formatter converts all input to uppercase. +class _UpperCaseTextFormatter extends TextInputFormatter { + @override + TextEditingValue formatEditUpdate( + TextEditingValue oldValue, TextEditingValue newValue) { + return TextEditingValue( + text: newValue.text.toUpperCase(), + selection: newValue.selection, + ); + } +} diff --git a/core/lib/presentation/views/pick_color/color_picker_action_buttons.dart b/core/lib/presentation/views/pick_color/color_picker_action_buttons.dart new file mode 100644 index 000000000..484845b75 --- /dev/null +++ b/core/lib/presentation/views/pick_color/color_picker_action_buttons.dart @@ -0,0 +1,427 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +/// Type of button used for OK or Cancel action button on a FlexColorPicker +/// dialog. +enum ColorPickerActionButtonType { + /// Use a [TextButton] button. + text, + + /// Use [OutlinedButton] button. + outlined, + + /// Use [ElevatedButton] button. + elevated, +} + +/// Used to define the order of OK and Cancel buttons on the FlexColorPicker +/// dialog. +enum ColorPickerActionButtonOrder { + /// OK is the right button. + okIsRight, + + /// OK is the left button. + okIsLeft, + + /// OK is on the place where it belongs depending on used platform + /// + /// Right: + /// + /// Cancel - OK : macOS, iOS, Android, Linux, Fuchsia + /// + /// Left: + /// + /// OK - Cancel : Windows + adaptive, +} + +/// Defines the FlexColorPicker OK and Cancel actions buttons and their style. +/// +/// You can define if action buttons are on a top toolbar or only +/// in the dialog bottom. The toolbar buttons are plain icon only +/// buttons. For the Dialog buttons you can choose between +/// [TextButton], [OutlinedButton] and [ElevatedButton]. +/// +/// The used icons can be changed form default ones, as can used tooltips. +/// +/// Used by FlexColorPicker to define action buttons and their style. +@immutable +class ColorPickerActionButtons with Diagnosticable { + /// Default const constructor. + const ColorPickerActionButtons({ + this.okButton = false, + this.closeButton = false, + this.okIcon = Icons.check, + this.closeIcon = Icons.close, + this.closeIsLast = true, + this.okTooltip, + this.closeTooltip, + this.closeTooltipIsClose = true, + this.toolIconsThemeData, + this.visualDensity, + this.padding = EdgeInsets.zero, + this.alignment = Alignment.center, + this.splashRadius = 24, + this.constraints = const BoxConstraints(minHeight: 42, minWidth: 42), + this.dialogActionButtons = true, + this.dialogActionOrder = ColorPickerActionButtonOrder.okIsRight, + this.dialogActionIcons = false, + this.dialogCancelButtonLabel, + this.dialogCancelButtonType = ColorPickerActionButtonType.text, + this.dialogOkButtonLabel, + this.dialogOkButtonType = ColorPickerActionButtonType.text, + this.useRootNavigator = true, + }); + + /// Dialog has an OK icon button on top toolbar to select active color + /// and close the color picker dialog. + /// + /// This will pop the current top route on navigation stack and return true. + /// Only enable this toolbar button when you are using the picker in a dialog. + /// + /// Defaults to false. + final bool okButton; + + /// Dialog has a Close icon button on top toolbar to cancel and close the + /// color picker dialog. + /// + /// This will pop the current top route on navigation stack and return false. + /// Only enable this toolbar button when you are using the picker in a dialog. + /// + /// Defaults to false. + final bool closeButton; + + /// Icon used for the OK action icon in the color picker dialog. + /// + /// Used both on the toolbar icon button and as a prefix icon on the bottom + /// action button, when icons have been enabled for the bottom action buttons. + /// + /// Defaults to [Icons.check]. + final IconData okIcon; + + /// Icon used for the close action icon in the color picker dialog. + /// + /// Used both on the toolbar icon button and as a prefix icon on the bottom + /// action button, when prefix icons have been enabled for the bottom + /// action buttons. + /// + /// Defaults to [Icons.close]. + final IconData closeIcon; + + /// Close icon is last in the color picker title toolbar. + /// + /// Set to false to swap the order of the OK and Close toolbar icons. + /// + /// Defaults to true, which results in the close 'x' icon being in upper + /// end corner of the picker dialog. + /// + /// Based on Material guide, the bottom action buttons in a dialog are always + /// in the Cancel-OK order. If the top toolbar buttons are used at the + /// same time, this value can be set to false to show the top toolbar buttons + /// in the same order as the Material bottom dialog action buttons. + /// + /// The recommendation is to not use the top and bottom action buttons at the + /// same time, but rather select one of the two options. The API does + /// however allow using both or even a mix and match. It is possible to show + /// **Cancel** and **OK** actions at the bottom of the dialog, and also add + /// just an 'x' icon in the upper end corner of the dialog that also + /// cancel-closes the dialog as expected. + final bool closeIsLast; + + /// Label used as tooltip for OK toolbar button. + /// + /// Provide your own or use the default material localization label. + /// + /// Defaults to MaterialLocalizations.of(context).okButtonLabel. + final String? okTooltip; + + /// Label used as tooltip for close toolbar button. + /// + /// Provide your own or use the default material localization label. + /// + /// Defaults to MaterialLocalizations.of(context).cancelButtonLabel if + /// closeTooltipIsClose is true. If false it defaults to + /// MaterialLocalizations.of(context).cancelButtonLabel. + final String? closeTooltip; + + /// Close toolbar icon button uses "close" material localization as + /// default label. + /// + /// If set to false, it uses "cancel" localization. You can still also + /// provide your own custom tooltip. This toggle is just convenient + /// if you want to call the top toolbar buttons that closes/cancels the + /// dialog "Close" (default) or "Cancel" like on a typical dialog bottom. + /// The "Close" tooltip, is more in line with the 'x' icon, + /// that here also closes the dialog with the dismiss result. + /// + /// Defaults to true. + final bool closeTooltipIsClose; + + /// The theme for the toolbar icons. + /// + /// The toolbar is compact, so icons are small by design. + /// + /// Effective style will uses any none null property in the passed in + /// [IconThemeData]. If the passed in theme data is null, or any property in + /// it, is null, then the following fallback defaults are used: + /// + /// color: remains null, so default [IconThemeData] color behavior is kept. + /// size: 22 + /// opacity: 0.90 + /// + /// NOTE: This theme on purpose does not merge any ambient theme. To keep the + /// compact fallback values as its default style. To change them you have + /// to pass in the desired [IconThemeData]. + final IconThemeData? toolIconsThemeData; + + /// Defines how compact the toolbar icon button layout will be. + /// + /// See also: + /// + /// * [ThemeData.visualDensity], which specifies the [visualDensity] for all + /// widgets within a [Theme]. + final VisualDensity? visualDensity; + + /// The padding around the toolbar icon buttons. The entire padded icon will + /// react to input gestures. + /// + /// Defaults to const EdgeInsets.all(0), + final EdgeInsetsGeometry padding; + + /// Defines how the icon is positioned within the IconButton. + /// + /// Defaults to [Alignment.center]. + final AlignmentGeometry alignment; + + /// The splash radius on the toolbar icon buttons. + /// + /// Defaults to 24. + final double splashRadius; + + /// Optional size constraints for the icon button. + /// + /// Defaults to: const BoxConstraints(minHeight: 34, minWidth: 34), + final BoxConstraints constraints; + + /// If set to false, the bottom dialog action buttons att the bottom + /// are removed. + /// + /// If you remove the bottom dialog action buttons, make sure to enabled the + /// ones in the dialog toolbar. + /// + /// Defaults to true. + final bool dialogActionButtons; + + /// Defines the order of the OK and Cancel actions buttons at the bottom + /// of the dialog. + /// + /// Options are: + /// + /// * okIsRight: OK is the right button. + /// * okIsLeft: OK is the left button. + /// * adaptive: OK is on the place where it belongs on used platform + /// Right: Cancel - OK : macOS, iOS, Android, Linux, Fuchsia + /// Left : OK - Cancel : Windows + /// + /// Defaults to "okIsRight". Prefer using "adaptive", but defaults to + /// "okIsRight" in order to not break past behavior. + final ColorPickerActionButtonOrder dialogActionOrder; + + /// If set to true, the dialog bottom action buttons will be prefixed with + /// an icon. + /// + /// The cancel buttons will get the [closeIcon] and the ok button will get + /// prefixed with the [okIcon]. + /// + /// Defaults to false. + final bool dialogActionIcons; + + /// Color picker dialog cancel button label. + /// + /// Label shown on the button for cancelling the color picking and closing + /// the dialog and returning false. + /// + /// If null, defaults to MaterialLocalizations.of(context).cancelButtonLabel. + final String? dialogCancelButtonLabel; + + /// Type of button used in the dialog for the Cancel button. + /// + /// The button will be themed based on closest ambient theme. + /// + /// Defaults to [ColorPickerActionButtonType.text] resulting in [TextButton]. + final ColorPickerActionButtonType dialogCancelButtonType; + + /// Color picker dialog OK button label. + /// + /// Label shown on bottom action button for selecting the current color in + /// the color picker dialog and closing the dialog and returning true. + /// + /// If null, defaults to MaterialLocalizations.of(context).okButtonLabel. + final String? dialogOkButtonLabel; + + /// Type of button used in the dialog for the OK button. + /// + /// The button will be themed based on closest ambient theme. + /// + /// Defaults to [ColorPickerActionButtonType.text] resulting in [TextButton]. + final ColorPickerActionButtonType dialogOkButtonType; + + /// The `useRootNavigator` argument is used to determine whether to push the + /// ColorPicker dialog to the [Navigator] furthest from or nearest to the + /// given `context`. + /// + /// By default, `useRootNavigator` is `true` and the dialog route created + /// by build of ColorPicker dialogs are on the root. + /// + /// This setting was moved here in version 2.1.0 in order to make the + /// property accessible by Navigator pop functions both in the ColorPicker + /// widget itself, as well as built-in dialogs that uses the ColorPicker. + final bool useRootNavigator; + + /// Copy the object with one or more provided properties changed. + ColorPickerActionButtons copyWith({ + bool? okButton, + bool? closeButton, + IconData? okIcon, + IconData? closeIcon, + bool? closeIsLast, + String? okTooltip, + String? closeTooltip, + bool? closeTooltipIsClose, + IconThemeData? toolIconsThemeData, + VisualDensity? visualDensity, + EdgeInsetsGeometry? padding, + AlignmentGeometry? alignment, + double? splashRadius, + BoxConstraints? constraints, + bool? dialogActionButtons, + ColorPickerActionButtonOrder? dialogActionOrder, + bool? dialogActionIcons, + String? dialogCancelButtonLabel, + ColorPickerActionButtonType? dialogCancelButtonType, + String? dialogOkButtonLabel, + ColorPickerActionButtonType? dialogOkButtonType, + bool? useRootNavigator, + }) { + return ColorPickerActionButtons( + okButton: okButton ?? this.okButton, + closeButton: closeButton ?? this.closeButton, + okIcon: okIcon ?? this.okIcon, + closeIcon: closeIcon ?? this.closeIcon, + closeIsLast: closeIsLast ?? this.closeIsLast, + okTooltip: okTooltip ?? this.okTooltip, + closeTooltip: closeTooltip ?? this.closeTooltip, + closeTooltipIsClose: closeTooltipIsClose ?? this.closeTooltipIsClose, + toolIconsThemeData: toolIconsThemeData ?? this.toolIconsThemeData, + visualDensity: visualDensity ?? this.visualDensity, + padding: padding ?? this.padding, + alignment: alignment ?? this.alignment, + splashRadius: splashRadius ?? this.splashRadius, + constraints: constraints ?? this.constraints, + dialogActionButtons: dialogActionButtons ?? this.dialogActionButtons, + dialogActionOrder: dialogActionOrder ?? this.dialogActionOrder, + dialogActionIcons: dialogActionIcons ?? this.dialogActionIcons, + dialogCancelButtonLabel: + dialogCancelButtonLabel ?? this.dialogCancelButtonLabel, + dialogCancelButtonType: + dialogCancelButtonType ?? this.dialogCancelButtonType, + dialogOkButtonLabel: dialogOkButtonLabel ?? this.dialogOkButtonLabel, + dialogOkButtonType: dialogOkButtonType ?? this.dialogOkButtonType, + useRootNavigator: useRootNavigator ?? this.useRootNavigator, + ); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other.runtimeType != runtimeType) return false; + return other is ColorPickerActionButtons && + runtimeType == other.runtimeType && + okButton == other.okButton && + closeButton == other.closeButton && + okIcon == other.okIcon && + closeIcon == other.closeIcon && + closeIsLast == other.closeIsLast && + okTooltip == other.okTooltip && + closeTooltip == other.closeTooltip && + closeTooltipIsClose == other.closeTooltipIsClose && + toolIconsThemeData == other.toolIconsThemeData && + visualDensity == other.visualDensity && + padding == other.padding && + alignment == other.alignment && + splashRadius == other.splashRadius && + constraints == other.constraints && + dialogActionButtons == other.dialogActionButtons && + dialogActionOrder == other.dialogActionOrder && + dialogActionIcons == other.dialogActionIcons && + dialogCancelButtonLabel == other.dialogCancelButtonLabel && + dialogCancelButtonType == other.dialogCancelButtonType && + dialogOkButtonLabel == other.dialogOkButtonLabel && + dialogOkButtonType == other.dialogOkButtonType && + useRootNavigator == other.useRootNavigator; + } + + @override + int get hashCode => Object.hashAll([ + okButton, + closeButton, + okIcon, + closeIcon, + closeIsLast, + okTooltip, + closeTooltip, + closeTooltipIsClose, + toolIconsThemeData, + visualDensity, + padding, + alignment, + splashRadius, + constraints, + dialogActionButtons, + dialogActionOrder, + dialogActionIcons, + dialogCancelButtonLabel, + dialogCancelButtonType, + dialogOkButtonLabel, + dialogOkButtonType, + useRootNavigator, + ]); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('okButton', okButton)); + properties.add(DiagnosticsProperty('closeButton', closeButton)); + properties.add(DiagnosticsProperty('okIcon', okIcon)); + properties.add(DiagnosticsProperty('closeIcon', closeIcon)); + properties.add(DiagnosticsProperty('closeIsLast', closeIsLast)); + properties.add(StringProperty('okTooltip', okTooltip)); + properties.add(StringProperty('closeTooltip', closeTooltip)); + properties.add( + DiagnosticsProperty('closeTooltipIsClose', closeTooltipIsClose)); + properties.add(DiagnosticsProperty( + 'toolIconsThemeData', toolIconsThemeData)); + properties.add( + DiagnosticsProperty('visualDensity', visualDensity)); + properties + .add(DiagnosticsProperty('alignment', alignment)); + properties.add(DoubleProperty('splashRadius', splashRadius)); + properties + .add(DiagnosticsProperty('constraints', constraints)); + properties.add( + DiagnosticsProperty('dialogActionButtons', dialogActionButtons)); + properties.add(EnumProperty( + 'dialogActionOrder', dialogActionOrder)); + properties + .add(DiagnosticsProperty('dialogActionIcons', dialogActionIcons)); + properties.add( + StringProperty('dialogCancelButtonLabel', dialogCancelButtonLabel)); + properties.add(EnumProperty( + 'dialogCancelButtonType', dialogCancelButtonType)); + properties.add(StringProperty('dialogOkButtonLabel', dialogOkButtonLabel)); + properties.add(EnumProperty( + 'dialogOkButtonType', dialogOkButtonType)); + properties + .add(DiagnosticsProperty('useRootNavigator', useRootNavigator)); + } +} diff --git a/core/lib/presentation/views/pick_color/color_picker_copy_past_behaivor.dart b/core/lib/presentation/views/pick_color/color_picker_copy_past_behaivor.dart new file mode 100644 index 000000000..39a3806d3 --- /dev/null +++ b/core/lib/presentation/views/pick_color/color_picker_copy_past_behaivor.dart @@ -0,0 +1,511 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +/// Enum that controls the RGB string format of the copied color value. +/// +/// When you copy a [Color] value from the color picker, this enum is +/// used to configure the desired default format of the received RGB string. +/// +/// If you have opacity enabled for the picker, the alpha value will not +/// be included in the copied RGB string if you use a format that does +/// not include the alpha value. +/// +/// When you paste a color string value into the color picker, it can +/// automatically parse a string from any of the available RGB strings formats +/// to a Dart and Flutter [Color] object, regardless of what the copy format is. +/// Additionally the pasted color value can be in the 3-char short RGB hex +/// format, it will also be correctly parsed to its [Color] value, provided +/// that [ColorPickerCopyPasteBehavior.parseShortHexCode] is true. +enum ColorPickerCopyFormat { + /// In Flutter/Dart Hex RGB format '0xAARRGGBB'. + dartCode, + + /// Hex RGB format with no alpha 'RRGGBB'. + hexRRGGBB, + + /// Hex RGB format with alpha 'AARRGGBB'. + hexAARRGGBB, + + /// Web Hex RGB format with leading num # sign and no alpha '#RRGGBB'. + numHexRRGGBB, + + /// Web Hex RGB format with leading num # sign and alpha '#AARRGGBB'. + numHexAARRGGBB, +} + +/// Used by FlexColorPicker to define how copy-paste operations behave. +/// +/// * Copy and paste action buttons in the top toolbar. +/// * Long press and/or right click copy and paste context menu. +/// * Ctrl-C and Ctrl-V keyboard shortcuts, also when not in edit field. +/// Keyboard shortcuts automatically uses Command instead of Ctrl on macOS. +/// * A copy color action button in the code entry and display field. +/// +/// You can also: +/// +/// * Define default result RGB string format of a copy command. +/// * Define icons for copy and paste action buttons. +/// * Define icon theme's for the copy and paste icons. +/// * Define paste color string parsing error feedback type and message if used. +/// * Modify the tooltips for copy and paste buttons. +/// +/// Paste operation supports all RGB string formats defined by +/// [ColorPickerCopyFormat], but copy format is only in selected +/// [copyFormat]. +@immutable +class ColorPickerCopyPasteBehavior with Diagnosticable { + /// Default constructor + const ColorPickerCopyPasteBehavior( + {this.ctrlC = true, + this.ctrlV = true, + this.autoFocus = true, + this.copyButton = false, + this.pasteButton = false, + this.copyIcon = Icons.copy, + this.pasteIcon = Icons.paste, + this.copyTooltip, + this.pasteTooltip, + this.copyFormat = ColorPickerCopyFormat.dartCode, + this.longPressMenu = false, + this.secondaryMenu = false, + this.secondaryOnDesktopLongOnDevice = false, + this.secondaryOnDesktopLongOnDeviceAndWeb = false, + this.editFieldCopyButton = true, + this.menuIconThemeData, + this.menuThemeData, + this.menuWidth = 80, + this.menuItemHeight = 30, + this.snackBarParseError = false, + this.snackBarMessage, + this.snackBarDuration = const Duration(milliseconds: 1800), + this.feedbackParseError = false, + this.parseShortHexCode = false, + this.editUsesParsedPaste = false}); + + /// A keyboard CMD/CTRL-C press will copy the clipboard into the picker. + /// + /// When enabled, this keyboard copy color shortcut works when the + /// ColorPicker and one of its focusable widgets have focus. Those include + /// color indicator, color field, buttons, opacity slider and the picker + /// selector as well as the color wheel. + /// + /// Defaults to true. + final bool ctrlC; + + /// A keyboard CMD/CTRL-V press will paste the clipboard into the picker. + /// + /// When enabled, this keyboard copy color shortcut works when the + /// ColorPicker and one of its focusable widgets have focus. Those include + /// color indicator, color field, buttons, opacity slider and the picker + /// selector as well as the color wheel. + /// + /// Defaults to true. + final bool ctrlV; + + /// When true, the picker tries to grab the focus when the picker is created. + /// + /// By default the picker tries to set focus to its own widgets when it is + /// created. It does this when either [ctrlC] or [ctrlV] are enabled in + /// order for the keyboard listener to be able to react to copy-paste events + /// even if no control on the widget has been focused yet. + /// + /// If you need another widget to retain focus. e.g. if the picker is used on + /// surface/scope shared with other widgets and not in its own dialog, then + /// setting [autoFocus] to false might help. + /// + /// If both [ctrlC] and [ctrlV] are false, the picker yields the focus the + /// same way as setting [autoFocus] false, but then you have no keyboard + /// shortcut copy-paste functions at all. With [autoFocus] false, you + /// can still use keyboard copy-paste shortcuts and yield the focus from + /// the picker. When you do this, the copy-paste keyboard shortcuts will not + /// work until one of the picker's components have been focused by + /// interacting with them. + /// + /// The picker still grabs focus when you click on its background, as one way + /// to set focus to keyboard listener to enable copy-paste keyboard shortcuts + /// or when you operate any of its controls, the control in question + /// always gains focus. + /// + /// Default to true. + final bool autoFocus; + + /// Show a copy action icon in the picker top tool bar. + /// + /// Defaults to false. + final bool copyButton; + + /// Show a paste action icon in the picker top tool bar. + /// + /// Defaults to false. + final bool pasteButton; + + /// Icon used for the copy action. + /// + /// The same COPY icon is used in the top tool bar, on the context menu and + /// after the code field, when those features are enabled. + /// + /// Defaults to [Icons.copy]. + final IconData copyIcon; + + /// Icon used for the paste action icon in the title bar. + /// + /// The same PASTE icon is used in the top tool bar and on the context menu, + /// when those features are enabled. + /// + /// Defaults to [Icons.paste]. + final IconData pasteIcon; + + /// Label used as tooltip for copy action. + /// + /// Provide your own or use the default material localization label. + /// + /// Defaults to MaterialLocalizations.of(context).copyButtonLabel. + /// If CTRL-C copying is also enabled, the string ' (CTRL-C)' is added + /// on Linux and Windows platforms and on macOS ' (CMD-C)' is added. + final String? copyTooltip; + + /// Label used as tooltip for paste action. + /// + /// Provide your own or use the default material localization label. + /// + /// Defaults to: MaterialLocalizations.of(context).pasteButtonLabel. + /// If CTRL-V pasting is also enabled, the string ' (CTRL-V)' is added + /// on Linux and Windows platforms and on macOS ' (CMD-V)' is added. + final String? pasteTooltip; + + /// Defines the format of the copied color code string. + /// + /// Defaults to [ColorPickerCopyFormat.dartCode]. + /// + /// * [ColorPickerCopyFormat.dartCode] is Flutter Hex RGB format '0xAARRGGBB'. + /// * [ColorPickerCopyFormat.hexRRGGBB] is Hex RGB format with no + /// alpha 'RRGGBB'. + /// * [ColorPickerCopyFormat.hexAARRGGBB] is Hex RGB format with + /// alpha 'AARRGGBB'. + /// * [ColorPickerCopyFormat.numHexRRGGBB] is Web Hex RGB format with a + /// leading num # sign and no alpha '#RRGGBB'. + /// * [ColorPickerCopyFormat.numHexAARRGGBB] is Web Hex RGB format with a + /// * leading num # sign and alpha '#AARRGGBB'. + final ColorPickerCopyFormat copyFormat; + + /// Use long press in the picker to open a color copy and paste menu. + /// + /// Defaults to false. + final bool longPressMenu; + + /// Use secondary button click in the picker to open a color copy and paste + /// menu. + /// + /// Defaults to false. + final bool secondaryMenu; + + /// Use secondary button click on desktop and their web version and long + /// press on iOs/Android devices in the picker, to open a color copy and + /// paste context menu. + /// + /// Defaults to false. + final bool secondaryOnDesktopLongOnDevice; + + /// Use secondary button click on desktop and long press on iOs/Android + /// devices and all web builds in the picker, to open a color copy and + /// paste context menu. + /// + /// Defaults to false. + final bool secondaryOnDesktopLongOnDeviceAndWeb; + + /// Show a copy button suffix in the color code edit and display field. + /// + /// Defaults to true. + final bool editFieldCopyButton; + + /// The theme for the menu icons. + /// + /// The menu is compact, so icons are small by design. + /// + /// Uses any none null property in passed in [IconThemeData]. If it is + /// is null, or any property in it is null, then it uses the + /// property values from surrounding `Theme.of(context).iconTheme` if they + /// are defined. For any values that remain null value, the following + /// fallback defaults are used: + /// ``` + /// color: remains null, so default [IconThemeData] color behavior is kept. + /// size: 16 + /// opacity: 0.90 + /// ``` + final IconThemeData? menuIconThemeData; + + /// The theme of the popup menu. + /// + /// Uses any none null property in provided [PopupMenuThemeData], if it is + /// null or any property in it is null, then it uses property values from + /// `Theme.of(context).popupMenuTheme` if they are not null, for any null + /// value the following fallback defaults are used: + /// ``` + /// color: theme.cardColor.withOpacity(0.9) + /// shape: RoundedRectangleBorder( + /// borderRadius: BorderRadius.circular(8), + /// side: BorderSide( + /// color: theme.dividerColor)) + /// elevation: 3 + /// textStyle: theme.textTheme.bodyMedium! + /// enableFeedback: true + /// ``` + final PopupMenuThemeData? menuThemeData; + + /// The width of the menu. + /// + /// Defaults to 80 dp. + final double menuWidth; + + /// The height of each menu item. + /// + /// Defaults to 30 dp. + final double menuItemHeight; + + /// Show a snack bar paste parse error message when pasting something that + /// could not be parsed to a color value. + /// + /// A paste parse error occurs when something is pasted into the color picker + /// that cannot parsed to a color value. + /// + /// Defaults to false. + final bool snackBarParseError; + + /// The message shown in the paste parse error snack bar. + /// + /// The String is shown in the snack bar when there + /// is a paste parse error and [snackBarParseError] is true. + /// + /// If null, it defaults to the combination of the two Material localization + /// labels `pasteButtonLabel`: `invalidDateFormatLabel` in a [Text] widget. + /// In English it says "Paste: Invalid format.". + /// + /// The snackBar uses the closest theme with SnackBarThemeData for its + /// theming. + final String? snackBarMessage; + + /// The duration the paste parse error snack bar message is shown. + /// + /// Defaults to const Duration(milliseconds: 1800). + final Duration snackBarDuration; + + /// If true then vibrate, play audible click or an alert sound, when a + /// paste parse error occurs. + /// + /// A paste parse error occurs when something is pasted into the color picker + /// that cannot parsed to a color value. + /// + /// This feature is experimental, its support is limited on most platforms + /// in Flutter. If Flutter one day supports the Material Sound Guide, this + /// feature can be improved with better sound effects. Currently it cannot be + /// improved without importing none SDK plugins/packages to make sounds. + /// This package strives to work without any plugins or packages, so it will + /// not add any additional none Flutter SDK imports. + /// + /// Defaults to false. + final bool feedbackParseError; + + /// When true the hex color code paste action and field entry parser, + /// interpret short three character web hex color codes like in CSS. + /// + /// Web allows for short HEX RGB color codes like 123, ABC, F0C and 5D1 + /// being used as RGB hex color values. These will be interpreted as + /// 112233, AABBCC, FF00CC and 55DD11 when [parseShortHexCode] is true. + /// This parsing applies to both pasted color values and entries in the color + /// code field when [parseShortHexCode] is true. + /// + /// Defaults to false. + final bool parseShortHexCode; + + /// If true, the color code entry field uses parsed paste action for + /// keyboard shortcuts CTRL-V and CMD-V, + /// + /// A standard text field, will just paste whatever text is in the copy/paste + /// buffer into the field. This is the `false` default behavior here too, + /// with the exception that the field only accepts valid hex value input + /// chars (0-9, A-F), so it always filters and pastes only the acceptable + /// input chars from the paste buffer. + /// + /// If this property is `true`, the edit field will use the same color + /// paste value parsing used by the other paste actions used when the input + /// field is not in focus. + /// This results in a paste action in the field that always fully replaces + /// the content with the parsed color value of the pasted data, not just + /// pasting in the string in the paste buffer over selected text. + /// + /// Currently this setting only impacts CTRL-V and CMD-V keyboard shortcut + /// pasting on desktops. The paste on Android and iOS are not intercepted + /// when this setting is true. + /// + /// Defaults to false. + /// + /// The false setting is equivalent to past versions (1.x) default behavior + /// when pasting strings into the code entry field. Setting the value to true + /// may be preferred for a more consistent paste experience. + final bool editUsesParsedPaste; + + /// Copy the object with one or more provided properties changed. + ColorPickerCopyPasteBehavior copyWith({ + bool? ctrlC, + bool? ctrlV, + bool? autoFocus, + bool? copyButton, + bool? pasteButton, + IconData? copyIcon, + IconData? pasteIcon, + String? copyTooltip, + String? pasteTooltip, + ColorPickerCopyFormat? copyFormat, + bool? longPressMenu, + bool? secondaryMenu, + bool? secondaryOnDesktopLongOnDevice, + bool? secondaryOnDesktopLongOnDeviceAndWeb, + bool? editFieldCopyButton, + IconThemeData? menuIconThemeData, + PopupMenuThemeData? menuThemeData, + double? menuWidth, + double? menuItemHeight, + bool? snackBarParseError, + String? snackBarMessage, + Duration? snackBarDuration, + bool? feedbackParseError, + bool? parseShortHexCode, + bool? editUsesParsedPaste, + }) { + return ColorPickerCopyPasteBehavior( + ctrlC: ctrlC ?? this.ctrlC, + ctrlV: ctrlV ?? this.ctrlV, + autoFocus: autoFocus ?? this.autoFocus, + copyButton: copyButton ?? this.copyButton, + pasteButton: pasteButton ?? this.pasteButton, + copyIcon: copyIcon ?? this.copyIcon, + pasteIcon: pasteIcon ?? this.pasteIcon, + copyTooltip: copyTooltip ?? this.copyTooltip, + pasteTooltip: pasteTooltip ?? this.pasteTooltip, + copyFormat: copyFormat ?? this.copyFormat, + longPressMenu: longPressMenu ?? this.longPressMenu, + secondaryMenu: secondaryMenu ?? this.secondaryMenu, + secondaryOnDesktopLongOnDevice: + secondaryOnDesktopLongOnDevice ?? this.secondaryOnDesktopLongOnDevice, + secondaryOnDesktopLongOnDeviceAndWeb: + secondaryOnDesktopLongOnDeviceAndWeb ?? + this.secondaryOnDesktopLongOnDeviceAndWeb, + editFieldCopyButton: editFieldCopyButton ?? this.editFieldCopyButton, + menuIconThemeData: menuIconThemeData ?? this.menuIconThemeData, + menuThemeData: menuThemeData ?? this.menuThemeData, + menuWidth: menuWidth ?? this.menuWidth, + menuItemHeight: menuItemHeight ?? this.menuItemHeight, + snackBarParseError: snackBarParseError ?? this.snackBarParseError, + snackBarMessage: snackBarMessage ?? this.snackBarMessage, + snackBarDuration: snackBarDuration ?? this.snackBarDuration, + feedbackParseError: feedbackParseError ?? this.feedbackParseError, + parseShortHexCode: parseShortHexCode ?? this.parseShortHexCode, + editUsesParsedPaste: editUsesParsedPaste ?? this.editUsesParsedPaste, + ); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other.runtimeType != runtimeType) return false; + return other is ColorPickerCopyPasteBehavior && + ctrlC == other.ctrlC && + ctrlV == other.ctrlV && + autoFocus == other.autoFocus && + copyButton == other.copyButton && + pasteButton == other.pasteButton && + copyIcon == other.copyIcon && + pasteIcon == other.pasteIcon && + copyTooltip == other.copyTooltip && + pasteTooltip == other.pasteTooltip && + copyFormat == other.copyFormat && + longPressMenu == other.longPressMenu && + secondaryMenu == other.secondaryMenu && + secondaryOnDesktopLongOnDevice == + other.secondaryOnDesktopLongOnDevice && + secondaryOnDesktopLongOnDeviceAndWeb == + other.secondaryOnDesktopLongOnDeviceAndWeb && + editFieldCopyButton == other.editFieldCopyButton && + menuIconThemeData == other.menuIconThemeData && + menuThemeData == other.menuThemeData && + menuWidth == other.menuWidth && + menuItemHeight == other.menuItemHeight && + snackBarParseError == other.snackBarParseError && + snackBarMessage == other.snackBarMessage && + snackBarDuration == other.snackBarDuration && + feedbackParseError == other.feedbackParseError && + parseShortHexCode == other.parseShortHexCode && + editUsesParsedPaste == other.editUsesParsedPaste; + } + + @override + int get hashCode => Object.hashAll([ + ctrlC, + ctrlV, + autoFocus, + copyButton, + pasteButton, + copyIcon, + pasteIcon, + copyTooltip, + pasteTooltip, + copyFormat, + longPressMenu, + secondaryMenu, + secondaryOnDesktopLongOnDevice, + secondaryOnDesktopLongOnDeviceAndWeb, + editFieldCopyButton, + menuIconThemeData, + menuThemeData, + menuWidth, + menuItemHeight, + snackBarParseError, + snackBarMessage, + snackBarDuration, + feedbackParseError, + parseShortHexCode, + editUsesParsedPaste, + ]); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('ctrlC', ctrlC)); + properties.add(DiagnosticsProperty('ctrlV', ctrlV)); + properties.add(DiagnosticsProperty('autoFocus', autoFocus)); + properties.add(DiagnosticsProperty('copyButton', copyButton)); + properties.add(DiagnosticsProperty('pasteButton', pasteButton)); + properties.add(DiagnosticsProperty('copyIcon', copyIcon)); + properties.add(DiagnosticsProperty('pasteIcon', pasteIcon)); + properties.add(StringProperty('copyTooltip', copyTooltip)); + properties.add(StringProperty('pasteTooltip', pasteTooltip)); + properties + .add(EnumProperty('copyFormat', copyFormat)); + properties.add(DiagnosticsProperty('longPressMenu', longPressMenu)); + properties.add(DiagnosticsProperty('secondaryMenu', secondaryMenu)); + properties.add(DiagnosticsProperty( + 'secondaryOnDesktopLongOnDevice', secondaryOnDesktopLongOnDevice)); + properties.add(DiagnosticsProperty( + 'secondaryOnDesktopLongOnDeviceAndWeb', + secondaryOnDesktopLongOnDeviceAndWeb)); + properties.add( + DiagnosticsProperty('editFieldCopyButton', editFieldCopyButton)); + properties.add(DiagnosticsProperty( + 'menuIconThemeData', menuIconThemeData)); + properties.add(DiagnosticsProperty( + 'menuThemeData', menuThemeData)); + properties.add(DoubleProperty('menuWidth', menuWidth)); + properties.add(DoubleProperty('menuItemHeight', menuItemHeight)); + properties.add( + DiagnosticsProperty('snackBarParseError', snackBarParseError)); + properties.add(StringProperty('snackBarMessage', snackBarMessage)); + properties.add( + DiagnosticsProperty('snackBarDuration', snackBarDuration)); + properties.add( + DiagnosticsProperty('feedbackParseError', feedbackParseError)); + properties + .add(DiagnosticsProperty('parseShortHexCode', parseShortHexCode)); + properties.add( + DiagnosticsProperty('editUsesParsedPaste', editUsesParsedPaste)); + } +} diff --git a/core/lib/presentation/views/pick_color/color_picker_extensions.dart b/core/lib/presentation/views/pick_color/color_picker_extensions.dart new file mode 100644 index 000000000..43f19880d --- /dev/null +++ b/core/lib/presentation/views/pick_color/color_picker_extensions.dart @@ -0,0 +1,214 @@ +import 'package:flutter/material.dart'; + +/// Extensions on non nullable [Color] to return it's color value as strings. +/// +/// The color extension also include getting a color's RGB hex code as a string +/// in two different formats. Extension [hexAlpha] returns a HEX code string of +/// a Color value including alpha channel. +/// The [hex] extension returns the hex color value as RGB string without the +/// alpha channel value. +extension FlexPickerNoNullColorExtensions on Color { + /// Return color's uppercase RGB hex string, including alpha channel. + String get hexAlpha { + return value.toRadixString(16).toUpperCase().padLeft(8, '0'); + } + + /// Return color's uppercase RGB hex string, excluding alpha channel. + String get hex { + return value.toRadixString(16).toUpperCase().padLeft(8, '0').substring(2); + } +} + +/// Extensions on [String]. +/// +/// Included extensions are, [toColor] to convert a String to a Color. +/// To [capitalize] the first letter in a String and [dotTail] to get +/// remaining string after first dot "." in a String. +extension FlexPickerNoNullStringExtensions on String { + // + /// Convert a HEX value encoded (A)RGB string to a Dart Color. + /// + /// * The string may include the '#' char, but does not have to. + /// * String may also include '0x' Dart Hex indicator, but does not have to. + /// * Any '#' '0x' patterns are trimmed out and String is assumed to be Hex. + /// * The String may start with alpha channel hex value, but does not have to, + /// if alpha value is missing "FF" is used for alpha. + /// * String may be longer than 8 chars, after trimming out # and 0x, it will + /// be RIGHT truncated to max 8 chars before parsing. + /// * If [enableShortRGB] is true a CSS style 3 char RGB value is interpreted + /// as RRGGBB, if false it used it as a partial color value. + /// + /// IF the resulting string cannot be parsed to a Color, is empty or null + /// THEN fully opaque black color is returned ELSE the Color is returned. + /// + /// To give caller a chance to handle parsing errors, use the same + /// extension on nullable Color [toColorMaybeNull]. It returns null when + /// there is a string that cannot be parsed to color value. + /// You can then decide what to do with the error instead of just receiving + /// fully opaque black color. + Color toColorShort(bool enableShortRGB) { + // If String was zero length, then we return transparent, cannot parse. + if (this == '') return const Color(0xFF000000); + // If String length is > 200 we as a safety precaution will not try to + // parse it to a color, for shorter lengths the last 8 chars will be used. + if (this.length > 200) return const Color(0xFF000000); + // Remove all num signs, we allow them, but disregard them all. + String hexColor = replaceAll('#', ''); + if (hexColor == '') return const Color(0xFF000000); + // Remove all spaces, we allow them, but disregard them all. + hexColor = hexColor.replaceAll(' ', ''); + if (hexColor == '') return const Color(0xFF000000); + // Remove all '0x' Hex code marks, we allow them, but disregard them all. + hexColor = hexColor.replaceAll('0x', ''); + if (hexColor == '') return const Color(0xFF000000); + // If the input is exactly 3 chars long, we may have a short Web hex code, + // let's make the potential 'RGB' code to a 'RRGGBB' code. + if (hexColor.length == 3 && enableShortRGB) { + hexColor = hexColor[0] + + hexColor[0] + + hexColor[1] + + hexColor[1] + + hexColor[2] + + hexColor[2]; + } + // Pad anything shorter than 7 with left 0 -> fill non spec channels with 0. + hexColor = hexColor.padLeft(6, '0'); + // Pad anything shorter than 9 with left F -> fill with opaque alpha. + hexColor = hexColor.padLeft(8, 'F'); + + // We only try to parse the last 8 chars in the remaining string, rest can + // still be whatever. + final int length = hexColor.length; + return Color(int.tryParse('0x${hexColor.substring(length - 8, length)}') ?? + 0xFF000000); + } + + /// Returns [toColorShort] with `enableShortRGB` set to true. + /// + /// Available for backwards compatibility with previous API. + Color get toColor => toColorShort(true); + + /// Capitalize the first letter in a string. + String get capitalize { + return (length > 1) ? this[0].toUpperCase() + substring(1) : toUpperCase(); + } + + /// Return the string remaining in a string after the last "." in a String, + /// if there is no "." the string itself is returned. + /// + /// This function can be used to e.g. return the enum tail value from an + /// enum's standard toString method. + String get dotTail { + return split('.').last; + } +} + +/// Extensions on [String]. +/// +/// Included extensions are, [toColorMaybeNull] to convert a String to a Color. +/// To [capitalizeMaybeNull] the first letter in a String and +/// [dotTailMaybeNull] to get remaining string after first dot "." in a String. +extension FlexPickerNullableStringExtensions on String? { + // + /// Convert a HEX value encoded (A)RGB string to a Dart Color. + /// + /// * The string may include the '#' char, but does not have to. + /// * String may also include '0x' Dart Hex indicator, but does not have to. + /// * Any '#' '0x' patterns are trimmed out and String is assumed to be Hex. + /// * The String may start with alpha channel hex value, but does not have to, + /// if alpha value is missing "FF" is used for alpha. + /// * String may be longer than 8 chars, after trimming out # and 0x, it will + /// be RIGHT truncated to max 8 chars before parsing. + /// * If [enableShortRGB] is true a CSS style 3 char RGB value is interpreted + /// as RRGGBB, if false it used it as a partial color value. + /// + /// IF the resulting string cannot be parsed to a Color, is empty or null + /// THEN NULL is returned ELSE the Color is returned. + Color? toColorShortMaybeNull(bool enableShortRGB) { + // If String was null or zero length, then we return null, cannot parse. + if (this == null || this == '') return null; + // If String length is > 200 we as a safety precaution will not try to + // parse it to a color, for shorter lengths the last 8 chars will be used. + if ((this?.length ?? 200) > 200) return null; + // Remove all num signs, we allow them, but disregard them all. + String hexColor = this?.replaceAll('#', '') ?? ''; + if (hexColor == '') return null; + // Remove all spaces, we allow them, but disregard them all. + hexColor = hexColor.replaceAll(' ', ''); + if (hexColor == '') return null; + // Remove all '0x' Hex code marks, we allow them, but disregard them all. + hexColor = hexColor.replaceAll('0x', ''); + if (hexColor == '') return null; + // If the input is exactly 3 chars long, we may have a short Web hex code, + // let's make the potential 'RGB' code to a 'RRGGBB' code. + if (hexColor.length == 3 && enableShortRGB) { + hexColor = hexColor[0] + + hexColor[0] + + hexColor[1] + + hexColor[1] + + hexColor[2] + + hexColor[2]; + } + // Pad anything shorter than 7 with left 0 -> fill non spec channels with 0. + hexColor = hexColor.padLeft(6, '0'); + // Pad anything shorter than 9 with left F -> fill with opaque alpha. + hexColor = hexColor.padLeft(8, 'F'); + + // We only try to parse the last 8 chars in the remaining string, rest can + // still be whatever. + final int length = hexColor.length; + final int? intColor = + int.tryParse('0x${hexColor.substring(length - 8, length)}'); + return intColor != null ? Color(intColor) : null; + } + + /// Returns [toColorShortMaybeNull] with `enableShortRGB` set to true. + /// + /// Available for backwards compatibility with previous API. + Color? get toColorMaybeNull => toColorShortMaybeNull(true); + + /// Capitalize the first letter in a string. If string is null, we get null + /// back. + String? get capitalizeMaybeNull { + if (this == null) { + return this; + } else { + return ((this?.length ?? 0) > 1) + ? (this?[0].toUpperCase() ?? '') + (this?.substring(1) ?? '') + : this?.toUpperCase() ?? ''; + } + } + + /// Return the string remaining in a string after the last "." in a String, + /// if there is no "." the string itself is returned. If string is null + /// we get null back. + /// + /// This function can be used to e.g. return the enum tail value from an + /// enum's standard toString method. + String? get dotTailMaybeNull { + if (this == null) { + return this; + } else { + return this?.split('.').last; + } + } +} + +/// Returns the control key label for the current platform. +/// +/// Windows, Linux: CTRL +/// Mac: CMD (Tried using the ⌘ symbol, did not show up on Web though.) +/// Others: Empty string +String platformControlKey(TargetPlatform platform, String key) { + switch (platform) { + case TargetPlatform.android: + case TargetPlatform.iOS: + case TargetPlatform.fuchsia: + return ''; + case TargetPlatform.linux: + case TargetPlatform.windows: + return ' (CTRL-$key)'; + case TargetPlatform.macOS: + return ' (CMD-$key)'; + } +} \ No newline at end of file diff --git a/core/lib/presentation/views/pick_color/dry_intrinsic.dart b/core/lib/presentation/views/pick_color/dry_intrinsic.dart new file mode 100644 index 000000000..597728910 --- /dev/null +++ b/core/lib/presentation/views/pick_color/dry_intrinsic.dart @@ -0,0 +1,77 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; + +// These two classes are a workaround to issue: +// https://github.com/flutter/flutter/issues/71687 +// This workaround for the issue was made by: +// @slightfoot and @matthew-carroll (GitHub accounts) + +/// Same as `IntrinsicWidth` except that when this widget is instructed +/// to `computeDryLayout()`, it doesn't invoke that on its child, instead +/// it computes the child's intrinsic width. +/// +/// This widget is useful in situations where the `child` does not +/// support dry layout, e.g., `TextField` as of 01/02/2021. +/// +/// Not library exposed, private to the package. +@immutable +class DryIntrinsicWidth extends SingleChildRenderObjectWidget { + /// Default const constructor. + const DryIntrinsicWidth({ + super.key, + required Widget super.child, + }); + + @override + _RenderDryIntrinsicWidth createRenderObject(BuildContext context) => + _RenderDryIntrinsicWidth(); +} + +class _RenderDryIntrinsicWidth extends RenderIntrinsicWidth { + @override + Size computeDryLayout(BoxConstraints constraints) { + if (child != null) { + final double? width = + child?.computeMinIntrinsicWidth(constraints.maxHeight); + final double? height = child?.computeMinIntrinsicHeight(width ?? 0); + return Size(width ?? 0, height ?? 0); + } else { + return Size.zero; + } + } +} + +/// Same as `IntrinsicHeight` except that when this widget is instructed +/// to `computeDryLayout()`, it doesn't invoke that on its child, instead +/// it computes the child's intrinsic height. +/// +/// This widget is useful in situations where the `child` does not +/// support dry layout, e.g., `TextField` as of 01/02/2021. +/// +/// Not library exposed, private to the library. +@immutable +class DryIntrinsicHeight extends SingleChildRenderObjectWidget { + /// Default const constructor. + const DryIntrinsicHeight({ + super.key, + required Widget super.child, + }); + + @override + _RenderDryIntrinsicHeight createRenderObject(BuildContext context) => + _RenderDryIntrinsicHeight(); +} + +class _RenderDryIntrinsicHeight extends RenderIntrinsicHeight { + @override + Size computeDryLayout(BoxConstraints constraints) { + if (child != null) { + final double? height = + child?.computeMinIntrinsicHeight(constraints.maxWidth); + final double? width = child?.computeMinIntrinsicWidth(height ?? 0); + return Size(width ?? 0, height ?? 0); + } else { + return Size.zero; + } + } +} diff --git a/core/pubspec.lock b/core/pubspec.lock index f9bd50fc9..a6408da6d 100644 --- a/core/pubspec.lock +++ b/core/pubspec.lock @@ -145,22 +145,6 @@ packages: url: "https://pub.dev" source: hosted version: "6.1.4" - flex_color_picker: - dependency: "direct main" - description: - name: flex_color_picker - sha256: "607c9fdb26be84d4a5a0931ab42a7eda725372e4f5ebaa2526ab6b22ead752f9" - url: "https://pub.dev" - source: hosted - version: "3.1.0" - flex_seed_scheme: - dependency: transitive - description: - name: flex_seed_scheme - sha256: "7058288ef97d348657ac95cea25d65a9aac181ca08387ede891fd7230ad7600f" - url: "https://pub.dev" - source: hosted - version: "1.2.3" flutter: dependency: "direct main" description: flutter @@ -575,4 +559,4 @@ packages: version: "6.2.2" sdks: dart: ">=2.19.2 <3.0.0" - flutter: ">=3.7.0" + flutter: ">=3.7.0-0" diff --git a/core/pubspec.yaml b/core/pubspec.yaml index cd8756f0c..a9a09cb7d 100644 --- a/core/pubspec.yaml +++ b/core/pubspec.yaml @@ -59,8 +59,6 @@ dependencies: flutter_keyboard_visibility: 5.4.0 - flex_color_picker: 3.1.0 - flutter_image_compress: 1.1.3 http_parser: 4.0.2 diff --git a/lib/features/composer/presentation/controller/base_rich_text_controller.dart b/lib/features/composer/presentation/controller/base_rich_text_controller.dart index 86aee1fff..d2e2b1b19 100644 --- a/lib/features/composer/presentation/controller/base_rich_text_controller.dart +++ b/lib/features/composer/presentation/controller/base_rich_text_controller.dart @@ -22,7 +22,7 @@ abstract class BaseRichTextController extends GetxController { ) async { await ColorPickerDialogBuilder( context, - currentColor, + ValueNotifier(currentColor), title: AppLocalizations.of(context).chooseAColor, textActionSetColor: AppLocalizations.of(context).setColor, textActionResetDefault: AppLocalizations.of(context).resetToDefault,