Merge pull request #644 from AppFlowy-IO/feat/insert-text-at-cursor

Feat: insert text at cursor
This commit is contained in:
Lucas.Xu 2022-07-19 20:00:40 +08:00 committed by GitHub
commit ab687b9b7f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 214 additions and 39 deletions

View File

@ -1,3 +1,5 @@
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/document/text_delta.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -81,6 +83,40 @@ extension on TextNode {
} }
} }
TextSelection? _globalSelectionToLocal(Node node, Selection? globalSel) {
if (globalSel == null) {
return null;
}
final nodePath = node.path;
if (!pathEquals(nodePath, globalSel.start.path)) {
return null;
}
if (globalSel.isCollapsed()) {
return TextSelection(
baseOffset: globalSel.start.offset, extentOffset: globalSel.end.offset);
} else {
if (pathEquals(globalSel.start.path, globalSel.end.path)) {
return TextSelection(
baseOffset: globalSel.start.offset,
extentOffset: globalSel.end.offset);
}
}
return null;
}
Selection? _localSelectionToGlobal(Node node, TextSelection? sel) {
if (sel == null) {
return null;
}
final nodePath = node.path;
return Selection(
start: Position(path: nodePath, offset: sel.baseOffset),
end: Position(path: nodePath, offset: sel.extentOffset),
);
}
class _TextNodeWidget extends StatefulWidget { class _TextNodeWidget extends StatefulWidget {
final Node node; final Node node;
final EditorState editorState; final EditorState editorState;
@ -95,37 +131,114 @@ class _TextNodeWidget extends StatefulWidget {
State<_TextNodeWidget> createState() => __TextNodeWidgetState(); State<_TextNodeWidget> createState() => __TextNodeWidgetState();
} }
String _textContentOfDelta(Delta delta) {
return delta.operations.fold("", (previousValue, element) {
if (element is TextInsert) {
return previousValue + element.content;
}
return previousValue;
});
}
class __TextNodeWidgetState extends State<_TextNodeWidget> class __TextNodeWidgetState extends State<_TextNodeWidget>
implements TextInputClient { implements DeltaTextInputClient {
final _focusNode = FocusNode(debugLabel: "input");
TextNode get node => widget.node as TextNode; TextNode get node => widget.node as TextNode;
EditorState get editorState => widget.editorState; EditorState get editorState => widget.editorState;
TextEditingValue get textEditingValue => const TextEditingValue();
TextInputConnection? _textInputConnection; TextInputConnection? _textInputConnection;
_backDeleteTextAtSelection(TextSelection? sel) {
if (sel == null) {
return;
}
if (sel.start == 0) {
return;
}
if (sel.isCollapsed) {
TransactionBuilder(editorState)
..deleteText(node, sel.start - 1, 1)
..commit();
} else {
TransactionBuilder(editorState)
..deleteText(node, sel.start, sel.extentOffset - sel.baseOffset)
..commit();
}
_setEditingStateFromGlobal();
}
_forwardDeleteTextAtSelection(TextSelection? sel) {
if (sel == null) {
return;
}
if (sel.isCollapsed) {
TransactionBuilder(editorState)
..deleteText(node, sel.start, 1)
..commit();
} else {
TransactionBuilder(editorState)
..deleteText(node, sel.start, sel.extentOffset - sel.baseOffset)
..commit();
}
_setEditingStateFromGlobal();
}
_setEditingStateFromGlobal() {
_textInputConnection?.setEditingState(TextEditingValue(
text: _textContentOfDelta(node.delta),
selection: _globalSelectionToLocal(node, editorState.cursorSelection) ??
const TextSelection.collapsed(offset: 0)));
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
SelectableText.rich( KeyboardListener(
TextSpan( focusNode: _focusNode,
children: node.toTextSpans(), onKeyEvent: ((value) {
if (value is KeyDownEvent || value is KeyRepeatEvent) {
final sel =
_globalSelectionToLocal(node, editorState.cursorSelection);
if (value.logicalKey.keyLabel == "Backspace") {
_backDeleteTextAtSelection(sel);
} else if (value.logicalKey.keyLabel == "Delete") {
_forwardDeleteTextAtSelection(sel);
}
}
}),
child: SelectableText.rich(
showCursor: true,
TextSpan(
children: node.toTextSpans(),
),
onTap: () {
_focusNode.requestFocus();
},
onSelectionChanged: ((selection, cause) {
_textInputConnection?.close();
_textInputConnection = TextInput.attach(
this,
const TextInputConfiguration(
enableDeltaModel: true,
inputType: TextInputType.multiline,
textCapitalization: TextCapitalization.sentences,
),
);
debugPrint('selection: $selection');
editorState.cursorSelection =
_localSelectionToGlobal(node, selection);
_textInputConnection
?..show()
..setEditingState(TextEditingValue(
text: _textContentOfDelta(node.delta),
selection: selection));
}),
), ),
onTap: () {
_textInputConnection?.close();
_textInputConnection = TextInput.attach(
this,
const TextInputConfiguration(
enableDeltaModel: false,
inputType: TextInputType.multiline,
textCapitalization: TextCapitalization.sentences,
),
);
_textInputConnection
?..show()
..setEditingState(textEditingValue);
},
), ),
if (node.children.isNotEmpty) if (node.children.isNotEmpty)
...node.children.map( ...node.children.map(
@ -152,7 +265,10 @@ class __TextNodeWidgetState extends State<_TextNodeWidget>
@override @override
// TODO: implement currentTextEditingValue // TODO: implement currentTextEditingValue
TextEditingValue? get currentTextEditingValue => textEditingValue; TextEditingValue? get currentTextEditingValue => TextEditingValue(
text: _textContentOfDelta(node.delta),
selection: _globalSelectionToLocal(node, editorState.cursorSelection) ??
const TextSelection.collapsed(offset: 0));
@override @override
void insertTextPlaceholder(Size size) { void insertTextPlaceholder(Size size) {
@ -161,7 +277,7 @@ class __TextNodeWidgetState extends State<_TextNodeWidget>
@override @override
void performAction(TextInputAction action) { void performAction(TextInputAction action) {
// TODO: implement performAction debugPrint('action:$action');
} }
@override @override
@ -186,7 +302,24 @@ class __TextNodeWidgetState extends State<_TextNodeWidget>
@override @override
void updateEditingValue(TextEditingValue value) { void updateEditingValue(TextEditingValue value) {
debugPrint(value.text); debugPrint('offset: ${value.selection}');
}
@override
void updateEditingValueWithDeltas(List<TextEditingDelta> textEditingDeltas) {
debugPrint(textEditingDeltas.toString());
for (final textDelta in textEditingDeltas) {
if (textDelta is TextEditingDeltaInsertion) {
TransactionBuilder(editorState)
..insertText(node, textDelta.insertionOffset, textDelta.textInserted)
..commit();
} else if (textDelta is TextEditingDeltaDeletion) {
TransactionBuilder(editorState)
..deleteText(node, textDelta.deletedRange.start,
textDelta.deletedRange.end - textDelta.deletedRange.start)
..commit();
}
}
} }
@override @override

View File

@ -34,6 +34,7 @@ class EditorState {
for (final op in transaction.operations) { for (final op in transaction.operations) {
_applyOperation(op); _applyOperation(op);
} }
cursorSelection = transaction.afterSelection;
} }
void _applyOperation(Operation op) { void _applyOperation(Operation op) {

View File

@ -19,10 +19,12 @@ import './operation.dart';
@immutable @immutable
class Transaction { class Transaction {
final UnmodifiableListView<Operation> operations; final UnmodifiableListView<Operation> operations;
final Selection? cursorSelection; final Selection? beforeSelection;
final Selection? afterSelection;
const Transaction({ const Transaction({
required this.operations, required this.operations,
this.cursorSelection, this.beforeSelection,
this.afterSelection,
}); });
} }

View File

@ -1,7 +1,9 @@
import 'dart:collection'; import 'dart:collection';
import 'dart:math';
import 'package:flowy_editor/editor_state.dart'; import 'package:flowy_editor/editor_state.dart';
import 'package:flowy_editor/document/node.dart'; import 'package:flowy_editor/document/node.dart';
import 'package:flowy_editor/document/path.dart'; import 'package:flowy_editor/document/path.dart';
import 'package:flowy_editor/document/position.dart';
import 'package:flowy_editor/document/text_delta.dart'; import 'package:flowy_editor/document/text_delta.dart';
import 'package:flowy_editor/document/attributes.dart'; import 'package:flowy_editor/document/attributes.dart';
import 'package:flowy_editor/document/selection.dart'; import 'package:flowy_editor/document/selection.dart';
@ -22,49 +24,86 @@ import './transaction.dart';
class TransactionBuilder { class TransactionBuilder {
final List<Operation> operations = []; final List<Operation> operations = [];
EditorState state; EditorState state;
Selection? cursorSelection; Selection? beforeSelection;
Selection? afterSelection;
TransactionBuilder(this.state); TransactionBuilder(this.state);
/// Commit the operations to the state
commit() { commit() {
final transaction = _finish(); final transaction = _finish();
state.apply(transaction); state.apply(transaction);
} }
void insertNode(Path path, Node node) { insertNode(Path path, Node node) {
cursorSelection = state.cursorSelection; beforeSelection = state.cursorSelection;
operations.add(InsertOperation(path: path, value: node)); add(InsertOperation(path: path, value: node));
} }
void updateNode(Node node, Attributes attributes) { updateNode(Node node, Attributes attributes) {
cursorSelection = state.cursorSelection; beforeSelection = state.cursorSelection;
operations.add(UpdateOperation( add(UpdateOperation(
path: node.path, path: node.path,
attributes: Attributes.from(node.attributes)..addAll(attributes), attributes: Attributes.from(node.attributes)..addAll(attributes),
oldAttributes: node.attributes, oldAttributes: node.attributes,
)); ));
} }
void deleteNode(Node node) { deleteNode(Node node) {
cursorSelection = state.cursorSelection; beforeSelection = state.cursorSelection;
operations.add(DeleteOperation(path: node.path, removedValue: node)); add(DeleteOperation(path: node.path, removedValue: node));
} }
void textEdit(TextNode node, Delta Function() f) { textEdit(TextNode node, Delta Function() f) {
cursorSelection = state.cursorSelection; beforeSelection = state.cursorSelection;
final path = node.path; final path = node.path;
final delta = f(); final delta = f();
final inverted = delta.invert(node.delta); final inverted = delta.invert(node.delta);
operations
.add(TextEditOperation(path: path, delta: delta, inverted: inverted)); add(TextEditOperation(path: path, delta: delta, inverted: inverted));
}
insertText(TextNode node, int index, String content) {
textEdit(node, () => Delta().retain(index).insert(content));
afterSelection = Selection.collapsed(
Position(path: node.path, offset: index + content.length));
}
formatText(TextNode node, int index, int length, Attributes attributes) {
textEdit(node, () => Delta().retain(index).retain(length, attributes));
}
deleteText(TextNode node, int index, int length) {
textEdit(node, () => Delta().retain(index).delete(length));
afterSelection =
Selection.collapsed(Position(path: node.path, offset: index));
}
add(Operation op) {
final Operation? last = operations.isEmpty ? null : operations.last;
if (last != null) {
if (op is TextEditOperation &&
last is TextEditOperation &&
pathEquals(op.path, last.path)) {
final newOp = TextEditOperation(
path: op.path,
delta: last.delta.compose(op.delta),
inverted: op.inverted.compose(last.inverted),
);
operations[operations.length - 1] = newOp;
return;
}
}
operations.add(op);
} }
Transaction _finish() { Transaction _finish() {
return Transaction( return Transaction(
operations: UnmodifiableListView(operations), operations: UnmodifiableListView(operations),
cursorSelection: cursorSelection, beforeSelection: beforeSelection,
afterSelection: afterSelection,
); );
} }
} }