Merge branch 'feat/flowy_editor' into feat/flowy_editor

This commit is contained in:
Lucas.Xu
2022-07-22 10:37:01 +08:00
committed by GitHub
4 changed files with 172 additions and 10 deletions

View File

@ -43,6 +43,7 @@ class __TextNodeWidgetState extends State<_TextNodeWidget>
implements DeltaTextInputClient { implements DeltaTextInputClient {
TextNode get node => widget.node as TextNode; TextNode get node => widget.node as TextNode;
EditorState get editorState => widget.editorState; EditorState get editorState => widget.editorState;
bool _metaKeyDown = false;
TextInputConnection? _textInputConnection; TextInputConnection? _textInputConnection;
@ -87,6 +88,16 @@ class __TextNodeWidgetState extends State<_TextNodeWidget>
} else if (event.logicalKey == LogicalKeyboardKey.delete) { } else if (event.logicalKey == LogicalKeyboardKey.delete) {
_forwardDeleteTextAtSelection(sel); _forwardDeleteTextAtSelection(sel);
return KeyEventResult.handled; return KeyEventResult.handled;
} else if (event.logicalKey == LogicalKeyboardKey.metaLeft ||
event.logicalKey == LogicalKeyboardKey.metaRight) {
_metaKeyDown = true;
} else if (event.logicalKey == LogicalKeyboardKey.keyZ && _metaKeyDown) {
editorState.undoManager.undo();
}
} else if (event is RawKeyUpEvent) {
if (event.logicalKey == LogicalKeyboardKey.metaLeft ||
event.logicalKey == LogicalKeyboardKey.metaRight) {
_metaKeyDown = false;
} }
} }
return KeyEventResult.ignored; return KeyEventResult.ignored;

View File

@ -1,24 +1,37 @@
import 'package:flowy_editor/document/node.dart'; import 'package:flowy_editor/document/node.dart';
import 'package:flowy_editor/operation/operation.dart'; import 'package:flowy_editor/operation/operation.dart';
import 'dart:async';
import 'package:flowy_editor/flowy_editor.dart';
import 'package:flowy_editor/undo_manager.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import './document/state_tree.dart';
import './document/selection.dart'; import './document/selection.dart';
import './operation/operation.dart';
import './operation/transaction.dart'; class ApplyOptions {
import './render/render_plugins.dart'; /// This flag indicates that
/// whether the transaction should be recorded into
/// the undo stack.
final bool recordUndo;
const ApplyOptions({
this.recordUndo = true,
});
}
class EditorState { class EditorState {
final StateTree document; final StateTree document;
final RenderPlugins renderPlugins; final RenderPlugins renderPlugins;
List<Node> selectedNodes = []; List<Node> selectedNodes = [];
final UndoManager undoManager = UndoManager();
Selection? cursorSelection; Selection? cursorSelection;
Timer? _debouncedSealHistoryItemTimer;
EditorState({ EditorState({
required this.document, required this.document,
required this.renderPlugins, required this.renderPlugins,
}); }) {
undoManager.state = this;
}
/// TODO: move to a better place. /// TODO: move to a better place.
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -31,14 +44,38 @@ class EditorState {
); );
} }
void apply(Transaction transaction) { apply(Transaction transaction,
[ApplyOptions options = const ApplyOptions()]) {
for (final op in transaction.operations) { for (final op in transaction.operations) {
_applyOperation(op); _applyOperation(op);
} }
cursorSelection = transaction.afterSelection; cursorSelection = transaction.afterSelection;
if (options.recordUndo) {
final undoItem = undoManager.getUndoHistoryItem();
undoItem.addAll(transaction.operations);
if (undoItem.beforeSelection == null &&
transaction.beforeSelection != null) {
undoItem.beforeSelection = transaction.beforeSelection;
}
undoItem.afterSelection = transaction.afterSelection;
_debouncedSealHistoryItem();
}
} }
void _applyOperation(Operation op) { _debouncedSealHistoryItem() {
_debouncedSealHistoryItemTimer?.cancel();
_debouncedSealHistoryItemTimer =
Timer(const Duration(milliseconds: 1000), () {
if (undoManager.undoStack.isNonEmpty) {
debugPrint('Seal history item');
final last = undoManager.undoStack.last;
last.seal();
}
});
}
_applyOperation(Operation op) {
if (op is InsertOperation) { if (op is InsertOperation) {
document.insert(op.path, op.value); document.insert(op.path, op.value);
} else if (op is UpdateOperation) { } else if (op is UpdateOperation) {

View File

@ -31,7 +31,7 @@ class TransactionBuilder {
/// Commit the operations to the state /// Commit the operations to the state
commit() { commit() {
final transaction = _finish(); final transaction = finish();
state.apply(transaction); state.apply(transaction);
} }
@ -99,7 +99,7 @@ class TransactionBuilder {
operations.add(op); operations.add(op);
} }
Transaction _finish() { Transaction finish() {
return Transaction( return Transaction(
operations: UnmodifiableListView(operations), operations: UnmodifiableListView(operations),
beforeSelection: beforeSelection, beforeSelection: beforeSelection,

View File

@ -0,0 +1,114 @@
import 'dart:collection';
import 'package:flowy_editor/document/selection.dart';
import 'package:flowy_editor/operation/operation.dart';
import 'package:flowy_editor/operation/transaction_builder.dart';
import 'package:flowy_editor/operation/transaction.dart';
import 'package:flowy_editor/editor_state.dart';
/// This class contains operations committed by users.
/// If a [HistoryItem] is not sealed, operations can be added sequentially.
/// Otherwise, the operations should be added to a new [HistoryItem].
class HistoryItem extends LinkedListEntry<HistoryItem> {
final List<Operation> operations = [];
Selection? beforeSelection;
Selection? afterSelection;
bool _sealed = false;
HistoryItem();
seal() {
_sealed = true;
}
bool get sealed => _sealed;
add(Operation op) {
operations.add(op);
}
addAll(Iterable<Operation> iterable) {
operations.addAll(iterable);
}
Transaction toTransaction(EditorState state) {
final builder = TransactionBuilder(state);
for (var i = operations.length - 1; i >= 0; i--) {
final operation = operations[i];
final inverted = operation.invert();
builder.add(inverted);
}
builder.afterSelection = beforeSelection;
builder.beforeSelection = afterSelection;
return builder.finish();
}
}
class FixedSizeStack {
final _list = LinkedList<HistoryItem>();
final int maxSize;
FixedSizeStack(this.maxSize);
push(HistoryItem stackItem) {
if (_list.length >= maxSize) {
_list.remove(_list.first);
}
_list.add(stackItem);
}
HistoryItem? pop() {
if (_list.isEmpty) {
return null;
}
final last = _list.last;
_list.remove(last);
return last;
}
HistoryItem get last => _list.last;
bool get isEmpty => _list.isEmpty;
bool get isNonEmpty => _list.isNotEmpty;
}
class UndoManager {
final FixedSizeStack undoStack;
final FixedSizeStack redoStack;
EditorState? state;
UndoManager([int stackSize = 20])
: undoStack = FixedSizeStack(stackSize),
redoStack = FixedSizeStack(stackSize);
HistoryItem getUndoHistoryItem() {
if (undoStack.isEmpty) {
final item = HistoryItem();
undoStack.push(item);
return item;
}
final last = undoStack.last;
if (last.sealed) {
final item = HistoryItem();
undoStack.push(item);
return item;
}
return last;
}
undo() {
final s = state;
if (s == null) {
return;
}
final historyItem = undoStack.pop();
if (historyItem == null) {
return;
}
final transaction = historyItem.toTransaction(s);
s.apply(transaction, const ApplyOptions(recordUndo: false));
}
}