feat: implement edit text style by command + x

This commit is contained in:
Lucas.Xu 2022-08-01 22:41:30 +08:00
parent 06cab949f2
commit 159fe63575
4 changed files with 194 additions and 63 deletions

View File

@ -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<TextInsert>();
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<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) {
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;
}
}
}

View File

@ -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<String, dynamic> attributes) {
final selection = editorState.service.selectionService.currentSelection;
final nodes = editorState.service.selectionService.currentSelectedNodes.value;
final textNodes = nodes.whereType<TextNode>().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;
}

View File

@ -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<TextNode>()
.toList();
final nodes = editorState.service.selectionService.currentSelectedNodes.value;
final textNodes = nodes.whereType<TextNode>().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<TextNode> 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();
}

View File

@ -422,14 +422,19 @@ class _FlowySelectionState extends State<FlowySelection>
// 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);
}
}