|
@@ -4,6 +4,7 @@ import 'package:appflowy_editor/src/extensions/url_launcher_extension.dart';
|
|
|
import 'package:appflowy_editor/src/flutter/overlay.dart';
|
|
|
import 'package:appflowy_editor/src/infra/clipboard.dart';
|
|
|
import 'package:appflowy_editor/src/infra/flowy_svg.dart';
|
|
|
+import 'package:appflowy_editor/src/render/color_menu/color_picker.dart';
|
|
|
import 'package:appflowy_editor/src/render/link_menu/link_menu.dart';
|
|
|
import 'package:appflowy_editor/src/extensions/text_node_extensions.dart';
|
|
|
import 'package:appflowy_editor/src/extensions/editor_state_extensions.dart';
|
|
@@ -264,6 +265,37 @@ List<ToolbarItem> defaultToolbarItems = [
|
|
|
editorState.editorStyle.highlightColorHex!,
|
|
|
),
|
|
|
),
|
|
|
+ ToolbarItem(
|
|
|
+ id: 'appflowy.toolbar.color',
|
|
|
+ type: 4,
|
|
|
+ tooltipsMessage: AppFlowyEditorLocalizations.current.color,
|
|
|
+ iconBuilder: (isHighlight) => Icon(
|
|
|
+ Icons.color_lens_outlined,
|
|
|
+ size: 14,
|
|
|
+ color: isHighlight ? Colors.lightBlue : Colors.white,
|
|
|
+ ),
|
|
|
+ validator: _showInBuiltInTextSelection,
|
|
|
+ highlightCallback: (editorState) =>
|
|
|
+ _allSatisfy(
|
|
|
+ editorState,
|
|
|
+ BuiltInAttributeKey.color,
|
|
|
+ (value) =>
|
|
|
+ value != null &&
|
|
|
+ value != _generateFontColorOptions(editorState).first.colorHex,
|
|
|
+ ) ||
|
|
|
+ _allSatisfy(
|
|
|
+ editorState,
|
|
|
+ BuiltInAttributeKey.backgroundColor,
|
|
|
+ (value) =>
|
|
|
+ value != null &&
|
|
|
+ value !=
|
|
|
+ _generateBackgroundColorOptions(editorState).first.colorHex,
|
|
|
+ ),
|
|
|
+ handler: (editorState, context) => showColorMenu(
|
|
|
+ context,
|
|
|
+ editorState,
|
|
|
+ ),
|
|
|
+ ),
|
|
|
];
|
|
|
|
|
|
ToolbarItemValidator _onlyShowInSingleTextSelection = (editorState) {
|
|
@@ -301,6 +333,8 @@ bool _allSatisfy(
|
|
|
}
|
|
|
|
|
|
OverlayEntry? _linkMenuOverlay;
|
|
|
+OverlayEntry? _colorMenuOverlay;
|
|
|
+
|
|
|
EditorState? _editorState;
|
|
|
bool _changeSelectionInner = false;
|
|
|
void showLinkMenu(
|
|
@@ -343,6 +377,7 @@ void showLinkMenu(
|
|
|
BuiltInAttributeKey.href,
|
|
|
);
|
|
|
}
|
|
|
+
|
|
|
_linkMenuOverlay = OverlayEntry(builder: (context) {
|
|
|
return Positioned(
|
|
|
top: matchRect.bottom + 5.0,
|
|
@@ -360,6 +395,7 @@ void showLinkMenu(
|
|
|
text,
|
|
|
textNode: textNode,
|
|
|
);
|
|
|
+
|
|
|
_dismissLinkMenu();
|
|
|
},
|
|
|
onCopyLink: () {
|
|
@@ -419,3 +455,211 @@ void _dismissLinkMenu() {
|
|
|
.removeListener(_dismissLinkMenu);
|
|
|
_editorState = null;
|
|
|
}
|
|
|
+
|
|
|
+void _dismissColorMenu() {
|
|
|
+ // workaround: SelectionService has been released after hot reload.
|
|
|
+ final isSelectionDisposed =
|
|
|
+ _editorState?.service.selectionServiceKey.currentState == null;
|
|
|
+ if (isSelectionDisposed) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (_editorState?.service.selectionService.currentSelection.value == null) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (_changeSelectionInner) {
|
|
|
+ _changeSelectionInner = false;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ _colorMenuOverlay?.remove();
|
|
|
+ _colorMenuOverlay = null;
|
|
|
+
|
|
|
+ _editorState?.service.scrollService?.enable();
|
|
|
+ _editorState?.service.keyboardService?.enable();
|
|
|
+ _editorState?.service.selectionService.currentSelection
|
|
|
+ .removeListener(_dismissColorMenu);
|
|
|
+ _editorState = null;
|
|
|
+}
|
|
|
+
|
|
|
+void showColorMenu(
|
|
|
+ BuildContext context,
|
|
|
+ EditorState editorState, {
|
|
|
+ Selection? customSelection,
|
|
|
+}) {
|
|
|
+ final rects = editorState.service.selectionService.selectionRects;
|
|
|
+ var maxBottom = 0.0;
|
|
|
+ late Rect matchRect;
|
|
|
+ for (final rect in rects) {
|
|
|
+ if (rect.bottom > maxBottom) {
|
|
|
+ maxBottom = rect.bottom;
|
|
|
+ matchRect = rect;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ final baseOffset =
|
|
|
+ editorState.renderBox?.localToGlobal(Offset.zero) ?? Offset.zero;
|
|
|
+ matchRect = matchRect.shift(-baseOffset);
|
|
|
+
|
|
|
+ _dismissColorMenu();
|
|
|
+ _editorState = editorState;
|
|
|
+
|
|
|
+ // Since the link menu will only show in single text selection,
|
|
|
+ // We get the text node directly instead of judging details again.
|
|
|
+ final selection = customSelection ??
|
|
|
+ editorState.service.selectionService.currentSelection.value;
|
|
|
+
|
|
|
+ final node = editorState.service.selectionService.currentSelectedNodes;
|
|
|
+ if (selection == null || node.isEmpty || node.first is! TextNode) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ final textNode = node.first as TextNode;
|
|
|
+
|
|
|
+ String? backgroundColorHex;
|
|
|
+ if (textNode.allSatisfyBackgroundColorInSelection(selection)) {
|
|
|
+ backgroundColorHex = textNode.getAttributeInSelection<String>(
|
|
|
+ selection,
|
|
|
+ BuiltInAttributeKey.backgroundColor,
|
|
|
+ );
|
|
|
+ }
|
|
|
+ String? fontColorHex;
|
|
|
+ if (textNode.allSatisfyFontColorInSelection(selection)) {
|
|
|
+ fontColorHex = textNode.getAttributeInSelection<String>(
|
|
|
+ selection,
|
|
|
+ BuiltInAttributeKey.color,
|
|
|
+ );
|
|
|
+ } else {
|
|
|
+ fontColorHex = editorState.editorStyle.textStyle?.color?.toHex();
|
|
|
+ }
|
|
|
+
|
|
|
+ final style = editorState.editorStyle;
|
|
|
+ _colorMenuOverlay = OverlayEntry(builder: (context) {
|
|
|
+ return Positioned(
|
|
|
+ top: matchRect.bottom + 5.0,
|
|
|
+ left: matchRect.left + 10,
|
|
|
+ child: Material(
|
|
|
+ color: Colors.transparent,
|
|
|
+ child: ColorPicker(
|
|
|
+ pickerBackgroundColor:
|
|
|
+ style.selectionMenuBackgroundColor ?? Colors.white,
|
|
|
+ pickerItemHoverColor: style.selectionMenuItemSelectedColor ??
|
|
|
+ Colors.blue.withOpacity(0.3),
|
|
|
+ pickerItemTextColor: style.selectionMenuItemTextColor ?? Colors.black,
|
|
|
+ selectedFontColorHex: fontColorHex,
|
|
|
+ selectedBackgroundColorHex: backgroundColorHex,
|
|
|
+ fontColorOptions: _generateFontColorOptions(editorState),
|
|
|
+ backgroundColorOptions: _generateBackgroundColorOptions(editorState),
|
|
|
+ onSubmittedbackgroundColorHex: (color) {
|
|
|
+ formatHighlightColor(
|
|
|
+ editorState,
|
|
|
+ color,
|
|
|
+ );
|
|
|
+ _dismissColorMenu();
|
|
|
+ },
|
|
|
+ onSubmittedFontColorHex: (color) {
|
|
|
+ formatFontColor(
|
|
|
+ editorState,
|
|
|
+ color,
|
|
|
+ );
|
|
|
+ _dismissColorMenu();
|
|
|
+ },
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ );
|
|
|
+ });
|
|
|
+ Overlay.of(context)?.insert(_colorMenuOverlay!);
|
|
|
+
|
|
|
+ editorState.service.scrollService?.disable();
|
|
|
+ editorState.service.keyboardService?.disable();
|
|
|
+ editorState.service.selectionService.currentSelection
|
|
|
+ .addListener(_dismissColorMenu);
|
|
|
+}
|
|
|
+
|
|
|
+List<ColorOption> _generateFontColorOptions(EditorState editorState) {
|
|
|
+ final defaultColor =
|
|
|
+ editorState.editorStyle.textStyle?.color ?? Colors.black; // black
|
|
|
+ return [
|
|
|
+ ColorOption(
|
|
|
+ colorHex: defaultColor.toHex(),
|
|
|
+ name: AppFlowyEditorLocalizations.current.fontColorDefault,
|
|
|
+ ),
|
|
|
+ ColorOption(
|
|
|
+ colorHex: Colors.grey.toHex(),
|
|
|
+ name: AppFlowyEditorLocalizations.current.fontColorGray,
|
|
|
+ ),
|
|
|
+ ColorOption(
|
|
|
+ colorHex: Colors.brown.toHex(),
|
|
|
+ name: AppFlowyEditorLocalizations.current.fontColorBrown,
|
|
|
+ ),
|
|
|
+ ColorOption(
|
|
|
+ colorHex: Colors.yellow.toHex(),
|
|
|
+ name: AppFlowyEditorLocalizations.current.fontColorYellow,
|
|
|
+ ),
|
|
|
+ ColorOption(
|
|
|
+ colorHex: Colors.green.toHex(),
|
|
|
+ name: AppFlowyEditorLocalizations.current.fontColorGreen,
|
|
|
+ ),
|
|
|
+ ColorOption(
|
|
|
+ colorHex: Colors.blue.toHex(),
|
|
|
+ name: AppFlowyEditorLocalizations.current.fontColorBlue,
|
|
|
+ ),
|
|
|
+ ColorOption(
|
|
|
+ colorHex: Colors.purple.toHex(),
|
|
|
+ name: AppFlowyEditorLocalizations.current.fontColorPurple,
|
|
|
+ ),
|
|
|
+ ColorOption(
|
|
|
+ colorHex: Colors.pink.toHex(),
|
|
|
+ name: AppFlowyEditorLocalizations.current.fontColorPink,
|
|
|
+ ),
|
|
|
+ ColorOption(
|
|
|
+ colorHex: Colors.red.toHex(),
|
|
|
+ name: AppFlowyEditorLocalizations.current.fontColorRed,
|
|
|
+ ),
|
|
|
+ ];
|
|
|
+}
|
|
|
+
|
|
|
+List<ColorOption> _generateBackgroundColorOptions(EditorState editorState) {
|
|
|
+ final defaultBackgroundColorHex =
|
|
|
+ editorState.editorStyle.highlightColorHex ?? '0x6000BCF0';
|
|
|
+ return [
|
|
|
+ ColorOption(
|
|
|
+ colorHex: defaultBackgroundColorHex,
|
|
|
+ name: AppFlowyEditorLocalizations.current.backgroundColorDefault,
|
|
|
+ ),
|
|
|
+ ColorOption(
|
|
|
+ colorHex: Colors.grey.withOpacity(0.3).toHex(),
|
|
|
+ name: AppFlowyEditorLocalizations.current.backgroundColorGray,
|
|
|
+ ),
|
|
|
+ ColorOption(
|
|
|
+ colorHex: Colors.brown.withOpacity(0.3).toHex(),
|
|
|
+ name: AppFlowyEditorLocalizations.current.backgroundColorBrown,
|
|
|
+ ),
|
|
|
+ ColorOption(
|
|
|
+ colorHex: Colors.yellow.withOpacity(0.3).toHex(),
|
|
|
+ name: AppFlowyEditorLocalizations.current.backgroundColorYellow,
|
|
|
+ ),
|
|
|
+ ColorOption(
|
|
|
+ colorHex: Colors.green.withOpacity(0.3).toHex(),
|
|
|
+ name: AppFlowyEditorLocalizations.current.backgroundColorGreen,
|
|
|
+ ),
|
|
|
+ ColorOption(
|
|
|
+ colorHex: Colors.blue.withOpacity(0.3).toHex(),
|
|
|
+ name: AppFlowyEditorLocalizations.current.backgroundColorBlue,
|
|
|
+ ),
|
|
|
+ ColorOption(
|
|
|
+ colorHex: Colors.purple.withOpacity(0.3).toHex(),
|
|
|
+ name: AppFlowyEditorLocalizations.current.backgroundColorPurple,
|
|
|
+ ),
|
|
|
+ ColorOption(
|
|
|
+ colorHex: Colors.pink.withOpacity(0.3).toHex(),
|
|
|
+ name: AppFlowyEditorLocalizations.current.backgroundColorPink,
|
|
|
+ ),
|
|
|
+ ColorOption(
|
|
|
+ colorHex: Colors.red.withOpacity(0.3).toHex(),
|
|
|
+ name: AppFlowyEditorLocalizations.current.backgroundColorRed,
|
|
|
+ ),
|
|
|
+ ];
|
|
|
+}
|
|
|
+
|
|
|
+extension on Color {
|
|
|
+ String toHex() {
|
|
|
+ return '0x${value.toRadixString(16)}';
|
|
|
+ }
|
|
|
+}
|