TF-1545: Update pick color dialog in rich text

(cherry picked from commit e70c9871c3e47ac2d0f2ed47265cae398665d984)
This commit is contained in:
HuyNguyen
2023-03-31 00:48:49 +07:00
committed by Dat Vu
parent c01c27b5b6
commit 3c761af4f4
10 changed files with 1739 additions and 57 deletions
@@ -192,6 +192,39 @@ extension AppColor on Color {
.toRadixString(16)
.padLeft(6, '0')
.toUpperCase()}';
static List<Color> 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<MaterialColor> 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<Color> get listShadeColors {
return [
const Color(0xFFFFFFFF),
const Color(0xFFFCFCFC),
shade50,
shade100,
shade200,
shade300,
shade400,
shade500,
shade600,
shade700,
shade800,
shade900
];
}
}
@@ -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<Color> _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, bool>{
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: <Widget>[
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,
),
),
);
}
}
@@ -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<Color> onColorChanged;
/// The Color code editing field has focus.
final ValueChanged<bool> 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<ColorCodeField> {
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: <TextInputFormatter>[
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<void> _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,
);
}
}
@@ -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(<Object?>[
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<bool>('okButton', okButton));
properties.add(DiagnosticsProperty<bool>('closeButton', closeButton));
properties.add(DiagnosticsProperty<IconData>('okIcon', okIcon));
properties.add(DiagnosticsProperty<IconData>('closeIcon', closeIcon));
properties.add(DiagnosticsProperty<bool>('closeIsLast', closeIsLast));
properties.add(StringProperty('okTooltip', okTooltip));
properties.add(StringProperty('closeTooltip', closeTooltip));
properties.add(
DiagnosticsProperty<bool>('closeTooltipIsClose', closeTooltipIsClose));
properties.add(DiagnosticsProperty<IconThemeData?>(
'toolIconsThemeData', toolIconsThemeData));
properties.add(
DiagnosticsProperty<VisualDensity?>('visualDensity', visualDensity));
properties
.add(DiagnosticsProperty<AlignmentGeometry>('alignment', alignment));
properties.add(DoubleProperty('splashRadius', splashRadius));
properties
.add(DiagnosticsProperty<BoxConstraints?>('constraints', constraints));
properties.add(
DiagnosticsProperty<bool>('dialogActionButtons', dialogActionButtons));
properties.add(EnumProperty<ColorPickerActionButtonOrder>(
'dialogActionOrder', dialogActionOrder));
properties
.add(DiagnosticsProperty<bool>('dialogActionIcons', dialogActionIcons));
properties.add(
StringProperty('dialogCancelButtonLabel', dialogCancelButtonLabel));
properties.add(EnumProperty<ColorPickerActionButtonType>(
'dialogCancelButtonType', dialogCancelButtonType));
properties.add(StringProperty('dialogOkButtonLabel', dialogOkButtonLabel));
properties.add(EnumProperty<ColorPickerActionButtonType>(
'dialogOkButtonType', dialogOkButtonType));
properties
.add(DiagnosticsProperty<bool>('useRootNavigator', useRootNavigator));
}
}
@@ -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(<Object?>[
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<bool>('ctrlC', ctrlC));
properties.add(DiagnosticsProperty<bool>('ctrlV', ctrlV));
properties.add(DiagnosticsProperty<bool>('autoFocus', autoFocus));
properties.add(DiagnosticsProperty<bool>('copyButton', copyButton));
properties.add(DiagnosticsProperty<bool>('pasteButton', pasteButton));
properties.add(DiagnosticsProperty<IconData>('copyIcon', copyIcon));
properties.add(DiagnosticsProperty<IconData>('pasteIcon', pasteIcon));
properties.add(StringProperty('copyTooltip', copyTooltip));
properties.add(StringProperty('pasteTooltip', pasteTooltip));
properties
.add(EnumProperty<ColorPickerCopyFormat>('copyFormat', copyFormat));
properties.add(DiagnosticsProperty<bool>('longPressMenu', longPressMenu));
properties.add(DiagnosticsProperty<bool>('secondaryMenu', secondaryMenu));
properties.add(DiagnosticsProperty<bool>(
'secondaryOnDesktopLongOnDevice', secondaryOnDesktopLongOnDevice));
properties.add(DiagnosticsProperty<bool>(
'secondaryOnDesktopLongOnDeviceAndWeb',
secondaryOnDesktopLongOnDeviceAndWeb));
properties.add(
DiagnosticsProperty<bool>('editFieldCopyButton', editFieldCopyButton));
properties.add(DiagnosticsProperty<IconThemeData?>(
'menuIconThemeData', menuIconThemeData));
properties.add(DiagnosticsProperty<PopupMenuThemeData?>(
'menuThemeData', menuThemeData));
properties.add(DoubleProperty('menuWidth', menuWidth));
properties.add(DoubleProperty('menuItemHeight', menuItemHeight));
properties.add(
DiagnosticsProperty<bool>('snackBarParseError', snackBarParseError));
properties.add(StringProperty('snackBarMessage', snackBarMessage));
properties.add(
DiagnosticsProperty<Duration>('snackBarDuration', snackBarDuration));
properties.add(
DiagnosticsProperty<bool>('feedbackParseError', feedbackParseError));
properties
.add(DiagnosticsProperty<bool>('parseShortHexCode', parseShortHexCode));
properties.add(
DiagnosticsProperty<bool>('editUsesParsedPaste', editUsesParsedPaste));
}
}
@@ -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)';
}
}
@@ -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;
}
}
}
+1 -17
View File
@@ -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"
-2
View File
@@ -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
@@ -22,7 +22,7 @@ abstract class BaseRichTextController extends GetxController {
) async {
await ColorPickerDialogBuilder(
context,
currentColor,
ValueNotifier<Color>(currentColor),
title: AppLocalizations.of(context).chooseAColor,
textActionSetColor: AppLocalizations.of(context).setColor,
textActionResetDefault: AppLocalizations.of(context).resetToDefault,