diff --git a/frontend/app_flowy/packages/flowy_editor/lib/extensions/text_node_extensions.dart b/frontend/app_flowy/packages/flowy_editor/lib/extensions/text_node_extensions.dart new file mode 100644 index 0000000000..29e90784ae --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/extensions/text_node_extensions.dart @@ -0,0 +1,88 @@ +import 'package:flowy_editor/document/node.dart'; +import 'package:flowy_editor/document/path.dart'; +import 'package:flowy_editor/document/position.dart'; +import 'package:flowy_editor/document/selection.dart'; +import 'package:flowy_editor/document/text_delta.dart'; +import 'package:flowy_editor/render/rich_text/rich_text_style.dart'; + +extension TextNodeExtension on TextNode { + bool allSatisfyBoldInSelection(Selection selection) => + allSatisfyInSelection(StyleKey.bold, selection); + + bool allSatisfyItalicInSelection(Selection selection) => + allSatisfyInSelection(StyleKey.italic, selection); + + bool allSatisfyUnderlineInSelection(Selection selection) => + allSatisfyInSelection(StyleKey.underline, selection); + + bool allSatisfyStrikethroughInSelection(Selection selection) => + allSatisfyInSelection(StyleKey.strikethrough, selection); + + bool allSatisfyInSelection(String styleKey, Selection selection) { + final ops = delta.operations.whereType(); + var start = 0; + for (final op in ops) { + if (start >= selection.end.offset) { + break; + } + final length = op.length; + if (start < selection.end.offset && + start + length > selection.start.offset) { + if (op.attributes == null || + !op.attributes!.containsKey(styleKey) || + op.attributes![styleKey] == false) { + return false; + } + } + start += length; + } + return true; + } +} + +extension TextNodesExtension on List { + bool allSatisfyBoldInSelection(Selection selection) => + allSatisfyInSelection(StyleKey.bold, selection); + + bool allSatisfyItalicInSelection(Selection selection) => + allSatisfyInSelection(StyleKey.italic, selection); + + bool allSatisfyUnderlineInSelection(Selection selection) => + allSatisfyInSelection(StyleKey.underline, selection); + + bool allSatisfyStrikethroughInSelection(Selection selection) => + allSatisfyInSelection(StyleKey.strikethrough, selection); + + bool allSatisfyInSelection(String styleKey, Selection selection) { + if (isEmpty) { + return false; + } + if (length == 1) { + return first.allSatisfyInSelection(styleKey, selection); + } else { + for (var i = 0; i < length; i++) { + final node = this[i]; + final Selection newSelection; + if (i == 0 && pathEquals(node.path, selection.start.path)) { + newSelection = selection.copyWith( + end: Position(path: node.path, offset: node.toRawString().length), + ); + } else if (i == length - 1 && + pathEquals(node.path, selection.end.path)) { + newSelection = selection.copyWith( + start: Position(path: node.path, offset: 0), + ); + } else { + newSelection = Selection( + start: Position(path: node.path, offset: 0), + end: Position(path: node.path, offset: node.toRawString().length), + ); + } + if (!node.allSatisfyInSelection(styleKey, newSelection)) { + return false; + } + } + return true; + } + } +} diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/default_text_operations/format_rich_text_style.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/default_text_operations/format_rich_text_style.dart new file mode 100644 index 0000000000..c88eab833f --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/default_text_operations/format_rich_text_style.dart @@ -0,0 +1,86 @@ +import 'package:flowy_editor/document/node.dart'; +import 'package:flowy_editor/editor_state.dart'; +import 'package:flowy_editor/operation/transaction_builder.dart'; + +bool formatRichTextStyle( + EditorState editorState, Map attributes) { + final selection = editorState.service.selectionService.currentSelection; + final nodes = editorState.service.selectionService.currentSelectedNodes.value; + final textNodes = nodes.whereType().toList(); + + if (selection == null || textNodes.isEmpty) { + return false; + } + + final builder = TransactionBuilder(editorState); + + // 1. All nodes are text nodes. + // 2. The first node is not TextNode. + // 3. The last node is not TextNode. + if (textNodes.length == nodes.length) { + if (textNodes.length == 1) { + builder.formatText( + textNodes.first, + selection.start.offset, + selection.end.offset - selection.start.offset, + attributes, + ); + } else { + for (var i = 0; i < textNodes.length; i++) { + final node = textNodes[i]; + if (i == 0) { + builder.formatText( + node, + selection.start.offset, + node.toRawString().length - selection.start.offset, + attributes, + ); + } else if (i == textNodes.length - 1) { + builder.formatText( + node, + 0, + selection.end.offset, + attributes, + ); + } else { + builder.formatText( + node, + 0, + node.toRawString().length, + attributes, + ); + } + } + } + } else { + for (var i = 0; i < textNodes.length; i++) { + final node = textNodes[i]; + if (i == 0 && node == nodes.first) { + builder.formatText( + node, + selection.start.offset, + node.toRawString().length - selection.start.offset, + attributes, + ); + } else if (i == textNodes.length - 1 && node == nodes.last) { + builder.formatText( + node, + 0, + selection.end.offset, + attributes, + ); + } else { + builder.formatText( + node, + 0, + node.toRawString().length, + attributes, + ); + } + } + } + + builder.commit(); + + return true; +} diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart index 5f13484442..6e4b742785 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart @@ -1,22 +1,21 @@ -import 'package:flowy_editor/document/node.dart'; -import 'package:flowy_editor/document/selection.dart'; -import 'package:flowy_editor/editor_state.dart'; -import 'package:flowy_editor/operation/transaction_builder.dart'; -import 'package:flowy_editor/service/keyboard_service.dart'; -import 'package:flowy_editor/render/rich_text/rich_text_style.dart'; import 'package:flutter/material.dart'; +import 'package:flowy_editor/document/node.dart'; +import 'package:flowy_editor/extensions/text_node_extensions.dart'; +import 'package:flowy_editor/render/rich_text/rich_text_style.dart'; +import 'package:flowy_editor/service/default_text_operations/format_rich_text_style.dart'; +import 'package:flowy_editor/service/keyboard_service.dart'; + FlowyKeyEventHandler updateTextStyleByCommandXHandler = (editorState, event) { if (!event.isMetaPressed || event.character == null) { return KeyEventResult.ignored; } final selection = editorState.service.selectionService.currentSelection; - final nodes = editorState.service.selectionService.currentSelectedNodes.value - .whereType() - .toList(); + final nodes = editorState.service.selectionService.currentSelectedNodes.value; + final textNodes = nodes.whereType().toList(growable: false); - if (selection == null || nodes.isEmpty) { + if (selection == null || textNodes.isEmpty) { return KeyEventResult.ignored; } @@ -24,7 +23,9 @@ FlowyKeyEventHandler updateTextStyleByCommandXHandler = (editorState, event) { // bold case 'B': case 'b': - _makeBold(editorState, nodes, selection); + formatRichTextStyle(editorState, { + StyleKey.bold: !textNodes.allSatisfyBoldInSelection(selection), + }); return KeyEventResult.handled; default: break; @@ -32,52 +33,3 @@ FlowyKeyEventHandler updateTextStyleByCommandXHandler = (editorState, event) { return KeyEventResult.ignored; }; - -// TODO: implement unBold. -void _makeBold( - EditorState editorState, List nodes, Selection selection) { - final builder = TransactionBuilder(editorState); - if (nodes.length == 1) { - builder.formatText( - nodes.first, - selection.start.offset, - selection.end.offset - selection.start.offset, - { - 'bold': true, - }, - ); - } else { - for (var i = 0; i < nodes.length; i++) { - final node = nodes[i]; - if (i == 0) { - builder.formatText( - node, - selection.start.offset, - node.toRawString().length - selection.start.offset, - { - 'bold': true, - }, - ); - } else if (i == nodes.length - 1) { - builder.formatText( - node, - 0, - selection.end.offset, - { - 'bold': true, - }, - ); - } else { - builder.formatText( - node, - 0, - node.toRawString().length, - { - 'bold': true, - }, - ); - } - } - } - builder.commit(); -} diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart index be1fd0bc8a..0695dd5e90 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart @@ -422,14 +422,19 @@ class _FlowySelectionState extends State // compute the selection in range. if (first != null && last != null) { - bool isDownward = panStartOffset!.dy <= panEndOffset!.dy; + bool isDownward; + if (first == last) { + isDownward = panStartOffset!.dx < panEndOffset!.dx; + } else { + isDownward = panStartOffset!.dy < panEndOffset!.dy; + } final start = first.getSelectionInRange(panStartOffset!, panEndOffset!).start; final end = last.getSelectionInRange(panStartOffset!, panEndOffset!).end; final selection = Selection( start: isDownward ? start : end, end: isDownward ? end : start); - debugPrint('[_onPanUpdate] $selection'); - editorState.updateCursorSelection(selection); + debugPrint('[_onPanUpdate] isDownward = $isDownward, $selection'); + editorState.service.selectionService.updateSelection(selection); } }