From 8c6c9f7c0de275e6bb159baadb857534c9771304 Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Tue, 19 Jul 2022 16:56:53 +0800 Subject: [PATCH] feat: transform betweens global/local cursor --- .../example/lib/plugin/text_node_widget.dart | 125 +++++++++++++++--- .../flowy_editor/lib/editor_state.dart | 1 + .../lib/operation/transaction.dart | 6 +- .../lib/operation/transaction_builder.dart | 20 ++- 4 files changed, 122 insertions(+), 30 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart index 517c0cda4b..edacd6716e 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart @@ -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:flutter/gestures.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 { final Node node; final EditorState editorState; @@ -106,37 +142,80 @@ String _textContentOfDelta(Delta delta) { class __TextNodeWidgetState extends State<_TextNodeWidget> implements DeltaTextInputClient { + final _focusNode = FocusNode(debugLabel: "input"); TextNode get node => widget.node as TextNode; EditorState get editorState => widget.editorState; - TextSelection? _localSelection; 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(); + } + + _textInputConnection?.setEditingState(TextEditingValue( + text: _textContentOfDelta(node.delta), + selection: _globalSelectionToLocal(node, editorState.cursorSelection) ?? + const TextSelection.collapsed(offset: 0))); + } + @override Widget build(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SelectableText.rich( - TextSpan( - children: node.toTextSpans(), - ), - onSelectionChanged: ((selection, cause) { - _textInputConnection?.close(); - _textInputConnection = TextInput.attach( - this, - const TextInputConfiguration( - enableDeltaModel: true, - inputType: TextInputType.multiline, - textCapitalization: TextCapitalization.sentences, - ), - ); - debugPrint('selection: $selection'); - _textInputConnection - ?..show() - ..setEditingState(TextEditingValue( - text: _textContentOfDelta(node.delta), selection: selection)); + KeyboardListener( + focusNode: _focusNode, + onKeyEvent: ((value) { + if (value is KeyDownEvent || value is KeyRepeatEvent) { + final sel = + _globalSelectionToLocal(node, editorState.cursorSelection); + if (value.logicalKey.keyLabel == "Backspace") { + _backDeleteTextAtSelection(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)); + }), + ), ), if (node.children.isNotEmpty) ...node.children.map( @@ -165,7 +244,8 @@ class __TextNodeWidgetState extends State<_TextNodeWidget> // TODO: implement currentTextEditingValue TextEditingValue? get currentTextEditingValue => TextEditingValue( text: _textContentOfDelta(node.delta), - selection: _localSelection ?? const TextSelection.collapsed(offset: -1)); + selection: _globalSelectionToLocal(node, editorState.cursorSelection) ?? + const TextSelection.collapsed(offset: 0)); @override void insertTextPlaceholder(Size size) { @@ -174,7 +254,7 @@ class __TextNodeWidgetState extends State<_TextNodeWidget> @override void performAction(TextInputAction action) { - // TODO: implement performAction + debugPrint('action:$action'); } @override @@ -204,6 +284,7 @@ class __TextNodeWidgetState extends State<_TextNodeWidget> @override void updateEditingValueWithDeltas(List textEditingDeltas) { + debugPrint(textEditingDeltas.toString()); for (final textDelta in textEditingDeltas) { if (textDelta is TextEditingDeltaInsertion) { TransactionBuilder(editorState) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart b/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart index 74c8a8e58c..521231a495 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart @@ -34,6 +34,7 @@ class EditorState { for (final op in transaction.operations) { _applyOperation(op); } + cursorSelection = transaction.afterSelection; } void _applyOperation(Operation op) { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction.dart b/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction.dart index fa56484ae3..3de528e868 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction.dart @@ -19,10 +19,12 @@ import './operation.dart'; @immutable class Transaction { final UnmodifiableListView operations; - final Selection? cursorSelection; + final Selection? beforeSelection; + final Selection? afterSelection; const Transaction({ required this.operations, - this.cursorSelection, + this.beforeSelection, + this.afterSelection, }); } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart b/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart index 9635c0ebc5..ec088dc25d 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart @@ -1,7 +1,9 @@ import 'dart:collection'; +import 'dart:math'; import 'package:flowy_editor/editor_state.dart'; 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/text_delta.dart'; import 'package:flowy_editor/document/attributes.dart'; import 'package:flowy_editor/document/selection.dart'; @@ -22,7 +24,8 @@ import './transaction.dart'; class TransactionBuilder { final List operations = []; EditorState state; - Selection? cursorSelection; + Selection? beforeSelection; + Selection? afterSelection; TransactionBuilder(this.state); @@ -33,12 +36,12 @@ class TransactionBuilder { } insertNode(Path path, Node node) { - cursorSelection = state.cursorSelection; + beforeSelection = state.cursorSelection; add(InsertOperation(path: path, value: node)); } updateNode(Node node, Attributes attributes) { - cursorSelection = state.cursorSelection; + beforeSelection = state.cursorSelection; add(UpdateOperation( path: node.path, attributes: Attributes.from(node.attributes)..addAll(attributes), @@ -47,12 +50,12 @@ class TransactionBuilder { } deleteNode(Node node) { - cursorSelection = state.cursorSelection; + beforeSelection = state.cursorSelection; add(DeleteOperation(path: node.path, removedValue: node)); } textEdit(TextNode node, Delta Function() f) { - cursorSelection = state.cursorSelection; + beforeSelection = state.cursorSelection; final path = node.path; final delta = f(); @@ -64,6 +67,8 @@ class TransactionBuilder { 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) { @@ -72,6 +77,8 @@ class TransactionBuilder { 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) { @@ -95,7 +102,8 @@ class TransactionBuilder { Transaction _finish() { return Transaction( operations: UnmodifiableListView(operations), - cursorSelection: cursorSelection, + beforeSelection: beforeSelection, + afterSelection: afterSelection, ); } }