mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
Merge branch 'feat/flowy_editor' into feat/flowy_editor
This commit is contained in:
@ -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;
|
||||||
|
@ -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) {
|
||||||
|
@ -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,
|
||||||
|
114
frontend/app_flowy/packages/flowy_editor/lib/undo_manager.dart
Normal file
114
frontend/app_flowy/packages/flowy_editor/lib/undo_manager.dart
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user