mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: transform betweens global/local cursor
This commit is contained in:
parent
abe0658cd3
commit
8c6c9f7c0d
@ -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<TextEditingDelta> textEditingDeltas) {
|
||||
debugPrint(textEditingDeltas.toString());
|
||||
for (final textDelta in textEditingDeltas) {
|
||||
if (textDelta is TextEditingDeltaInsertion) {
|
||||
TransactionBuilder(editorState)
|
||||
|
@ -34,6 +34,7 @@ class EditorState {
|
||||
for (final op in transaction.operations) {
|
||||
_applyOperation(op);
|
||||
}
|
||||
cursorSelection = transaction.afterSelection;
|
||||
}
|
||||
|
||||
void _applyOperation(Operation op) {
|
||||
|
@ -19,10 +19,12 @@ import './operation.dart';
|
||||
@immutable
|
||||
class Transaction {
|
||||
final UnmodifiableListView<Operation> operations;
|
||||
final Selection? cursorSelection;
|
||||
final Selection? beforeSelection;
|
||||
final Selection? afterSelection;
|
||||
|
||||
const Transaction({
|
||||
required this.operations,
|
||||
this.cursorSelection,
|
||||
this.beforeSelection,
|
||||
this.afterSelection,
|
||||
});
|
||||
}
|
||||
|
@ -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<Operation> 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user