chore: resolve conflicts.

This commit is contained in:
Lucas.Xu 2022-07-29 10:20:48 +08:00
commit ec83a68602
19 changed files with 597 additions and 86 deletions

View File

@ -2,23 +2,21 @@
"[dart]": {
"editor.formatOnSave": true,
"editor.formatOnType": true,
"editor.rulers": [
120
],
"editor.rulers": [80],
"editor.selectionHighlight": false,
"editor.suggest.snippetsPreventQuickSuggestions": false,
"editor.suggestSelection": "first",
"editor.tabCompletion": "onlySnippets",
"editor.wordBasedSuggestions": false
"editor.wordBasedSuggestions": false,
},
"svgviewer.enableautopreview": true,
"svgviewer.previewcolumn": "Active",
"svgviewer.showzoominout": true,
"editor.wordWrapColumn": 120,
"editor.wordWrapColumn": 80,
"editor.minimap.maxColumn": 140,
"prettier.printWidth": 140,
"editor.wordWrap": "wordWrapColumn",
"dart.lineLength": 120,
"dart.lineLength": 80,
"files.associations": {
"*.log.*": "log"
},

View File

@ -0,0 +1,45 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "example",
"cwd": "example",
"request": "launch",
"type": "dart"
},
{
"name": "example (profile mode)",
"cwd": "example",
"request": "launch",
"type": "dart",
"flutterMode": "profile"
},
{
"name": "example (release mode)",
"cwd": "example",
"request": "launch",
"type": "dart",
"flutterMode": "release"
},
{
"name": "flowy_editor",
"request": "launch",
"type": "dart"
},
{
"name": "flowy_editor (profile mode)",
"request": "launch",
"type": "dart",
"flutterMode": "profile"
},
{
"name": "flowy_editor (release mode)",
"request": "launch",
"type": "dart",
"flutterMode": "release"
},
]
}

View File

@ -63,6 +63,11 @@ class __ImageNodeWidgetState extends State<_ImageNodeWidget> with Selectable {
throw UnimplementedError();
}
@override
Offset localToGlobal(Offset offset) {
throw UnimplementedError();
}
@override
Rect getCursorRectInPosition(Position position) {
// TODO: implement getCursorRectInPosition

View File

@ -70,6 +70,11 @@ class _SelectedTextNodeWidgetState extends State<_SelectedTextNodeWidget>
);
}
@override
Offset localToGlobal(Offset offset) {
return _renderParagraph.localToGlobal(offset);
}
@override
List<Rect> getRectsInSelection(Selection selection) {
assert(pathEquals(selection.start.path, selection.end.path));

View File

@ -32,7 +32,11 @@ class Node extends ChangeNotifier with LinkedListEntry<Node> {
required this.children,
required this.attributes,
this.parent,
});
}) {
for (final child in children) {
child.parent = this;
}
}
factory Node.fromJson(Map<String, Object> json) {
assert(json['type'] is String);

View File

@ -38,7 +38,22 @@ class EditorState {
final service = FlowyService();
final UndoManager undoManager = UndoManager();
Selection? cursorSelection;
Selection? _cursorSelection;
Selection? get cursorSelection {
return _cursorSelection;
}
/// add the set reason in the future, don't use setter
updateCursorSelection(Selection? cursorSelection) {
// broadcast to other users here
if (cursorSelection == null) {
service.selectionService.clearSelection();
} else {
service.selectionService.updateSelection(cursorSelection);
}
_cursorSelection = cursorSelection;
}
Timer? _debouncedSealHistoryItemTimer;
@ -74,7 +89,7 @@ class EditorState {
for (final op in transaction.operations) {
_applyOperation(op);
}
cursorSelection = transaction.afterSelection;
updateCursorSelection(transaction.afterSelection);
if (options.recordUndo) {
final undoItem = undoManager.getUndoHistoryItem();

View File

@ -1,21 +1,27 @@
import 'package:flowy_editor/document/path.dart';
import 'package:flowy_editor/document/node.dart';
import 'package:flowy_editor/document/text_delta.dart';
import 'package:flowy_editor/document/attributes.dart';
import 'package:flowy_editor/flowy_editor.dart';
abstract class Operation {
final Path path;
Operation({required this.path});
Operation copyWithPath(Path path);
Operation invert();
}
class InsertOperation extends Operation {
final Path path;
final Node value;
InsertOperation({
required this.path,
required super.path,
required this.value,
});
InsertOperation copyWith({Path? path, Node? value}) =>
InsertOperation(path: path ?? this.path, value: value ?? this.value);
@override
Operation copyWithPath(Path path) => copyWith(path: path);
@override
Operation invert() {
return DeleteOperation(
@ -26,16 +32,25 @@ class InsertOperation extends Operation {
}
class UpdateOperation extends Operation {
final Path path;
final Attributes attributes;
final Attributes oldAttributes;
UpdateOperation({
required this.path,
required super.path,
required this.attributes,
required this.oldAttributes,
});
UpdateOperation copyWith(
{Path? path, Attributes? attributes, Attributes? oldAttributes}) =>
UpdateOperation(
path: path ?? this.path,
attributes: attributes ?? this.attributes,
oldAttributes: oldAttributes ?? this.oldAttributes);
@override
Operation copyWithPath(Path path) => copyWith(path: path);
@override
Operation invert() {
return UpdateOperation(
@ -47,14 +62,19 @@ class UpdateOperation extends Operation {
}
class DeleteOperation extends Operation {
final Path path;
final Node removedValue;
DeleteOperation({
required this.path,
required super.path,
required this.removedValue,
});
DeleteOperation copyWith({Path? path, Node? removedValue}) => DeleteOperation(
path: path ?? this.path, removedValue: removedValue ?? this.removedValue);
@override
Operation copyWithPath(Path path) => copyWith(path: path);
@override
Operation invert() {
return InsertOperation(
@ -65,18 +85,64 @@ class DeleteOperation extends Operation {
}
class TextEditOperation extends Operation {
final Path path;
final Delta delta;
final Delta inverted;
TextEditOperation({
required this.path,
required super.path,
required this.delta,
required this.inverted,
});
TextEditOperation copyWith({Path? path, Delta? delta, Delta? inverted}) =>
TextEditOperation(
path: path ?? this.path,
delta: delta ?? this.delta,
inverted: inverted ?? this.inverted);
@override
Operation copyWithPath(Path path) => copyWith(path: path);
@override
Operation invert() {
return TextEditOperation(path: path, delta: inverted, inverted: delta);
}
}
Path transformPath(Path preInsertPath, Path b, [int delta = 1]) {
if (preInsertPath.length > b.length) {
return b;
}
if (preInsertPath.isEmpty || b.isEmpty) {
return b;
}
// check the prefix
for (var i = 0; i < preInsertPath.length - 1; i++) {
if (preInsertPath[i] != b[i]) {
return b;
}
}
final prefix = preInsertPath.sublist(0, preInsertPath.length - 1);
final suffix = b.sublist(preInsertPath.length);
final preInsertLast = preInsertPath.last;
final bAtIndex = b[preInsertPath.length - 1];
if (preInsertLast <= bAtIndex) {
prefix.add(bAtIndex + delta);
} else {
prefix.add(bAtIndex);
}
prefix.addAll(suffix);
return prefix;
}
Operation transformOperation(Operation a, Operation b) {
if (a is InsertOperation) {
final newPath = transformPath(a.path, b.path);
return b.copyWithPath(newPath);
} else if (b is DeleteOperation) {
final newPath = transformPath(a.path, b.path, -1);
return b.copyWithPath(newPath);
}
// TODO: transform update and textedit
return b;
}

View File

@ -3,19 +3,15 @@ import 'package:flutter/material.dart';
import 'package:flowy_editor/document/selection.dart';
import './operation.dart';
/// This class to use to store the **changes**
/// will be applied to the editor.
/// A [Transaction] has a list of [Operation] objects that will be applied
/// to the editor. It is an immutable class and used to store and transmit.
///
/// This class is immutable version the the class
/// [[Transaction]]. Is used to stored and
/// transmit. If you want to build the transaction,
/// use [[Transaction]] directly.
/// If you want to build a new [Transaction], use [TransactionBuilder] directly.
///
/// There will be several ways to consume the transaction:
/// 1. Apply to the state to update the UI.
/// 2. Send to the backend to store and do operation transforming.
/// 3. Stored by the UndoManager to implement redo/undo.
///
/// 3. Used by the UndoManager to implement redo/undo.
@immutable
class Transaction {
final UnmodifiableListView<Operation> operations;

View File

@ -11,16 +11,10 @@ import 'package:flowy_editor/document/selection.dart';
import './operation.dart';
import './transaction.dart';
///
/// This class is used to
/// build the transaction from the state.
///
/// This class automatically save the
/// cursor from the state.
///
/// When the transaction is undo, the
/// cursor can be restored.
///
/// A [TransactionBuilder] is used to build the transaction from the state.
/// It will save make a snapshot of the cursor selection state automatically.
/// The cursor can be resoted if the transaction is undo.
class TransactionBuilder {
final List<Operation> operations = [];
EditorState state;
@ -96,6 +90,9 @@ class TransactionBuilder {
return;
}
}
for (var i = 0; i < operations.length; i++) {
op = transformOperation(operations[i], op);
}
operations.add(op);
}

View File

@ -22,6 +22,11 @@ mixin DefaultSelectable {
Selection getSelectionInRange(Offset start, Offset end) =>
forward.getSelectionInRange(start, end);
Offset localToGlobal(Offset offset) => forward.localToGlobal(offset);
Selection? getWorldBoundaryInOffset(Offset offset) =>
forward.getWorldBoundaryInOffset(offset);
Position start() => forward.start();
Position end() => forward.end();

View File

@ -92,6 +92,16 @@ class _FlowyRichTextState extends State<FlowyRichText> with Selectable {
return Position(path: widget.textNode.path, offset: baseOffset);
}
@override
Selection? getWorldBoundaryInOffset(Offset offset) {
final localOffset = _renderParagraph.globalToLocal(offset);
final textPosition = _renderParagraph.getPositionForOffset(localOffset);
final textRange = _renderParagraph.getWordBoundary(textPosition);
final start = Position(path: widget.textNode.path, offset: textRange.start);
final end = Position(path: widget.textNode.path, offset: textRange.end);
return Selection(start: start, end: end);
}
@override
List<Rect> getRectsInSelection(Selection selection) {
assert(pathEquals(selection.start.path, selection.end.path) &&
@ -155,6 +165,11 @@ class _FlowyRichTextState extends State<FlowyRichText> with Selectable {
);
}
@override
Offset localToGlobal(Offset offset) {
return _renderParagraph.localToGlobal(offset);
}
TextSpan get _textSpan => TextSpan(
children: widget.textNode.delta.operations
.whereType<TextInsert>()

View File

@ -4,7 +4,6 @@ import 'package:flowy_editor/infra/flowy_svg.dart';
import 'package:flowy_editor/render/node_widget_builder.dart';
import 'package:flowy_editor/render/rich_text/default_selectable.dart';
import 'package:flowy_editor/render/rich_text/flowy_rich_text.dart';
import 'package:flowy_editor/render/rich_text/rich_text_style.dart';
import 'package:flowy_editor/render/selection/selectable.dart';
import 'package:flutter/material.dart';

View File

@ -17,10 +17,10 @@ class CursorWidget extends StatefulWidget {
final LayerLink layerLink;
@override
State<CursorWidget> createState() => _CursorWidgetState();
State<CursorWidget> createState() => CursorWidgetState();
}
class _CursorWidgetState extends State<CursorWidget> {
class CursorWidgetState extends State<CursorWidget> {
bool showCursor = true;
late Timer timer;
@ -28,7 +28,17 @@ class _CursorWidgetState extends State<CursorWidget> {
void initState() {
super.initState();
timer = Timer.periodic(
timer = _initTimer();
}
@override
void dispose() {
timer.cancel();
super.dispose();
}
Timer _initTimer() {
return Timer.periodic(
Duration(milliseconds: (widget.blinkingInterval * 1000).toInt()),
(timer) {
setState(() {
@ -37,10 +47,13 @@ class _CursorWidgetState extends State<CursorWidget> {
});
}
@override
void dispose() {
/// force the cursor widget to show for a while
show() {
setState(() {
showCursor = true;
});
timer.cancel();
super.dispose();
timer = _initTimer();
}
@override

View File

@ -21,8 +21,14 @@ mixin Selectable<T extends StatefulWidget> on State<T> {
///
/// The return result must be an offset of the local coordinate system.
Position getPositionInOffset(Offset start);
Selection? getWorldBoundaryInOffset(Offset start) {
return null;
}
Rect getCursorRectInPosition(Position position);
Offset localToGlobal(Offset offset);
Position start();
Position end();

View File

@ -1,14 +1,143 @@
import 'package:flowy_editor/flowy_editor.dart';
import 'package:flowy_editor/service/keyboard_service.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
FlowyKeyEventHandler arrowKeysHandler = (editorState, event) {
if (event.logicalKey != LogicalKeyboardKey.arrowUp &&
event.logicalKey != LogicalKeyboardKey.arrowDown &&
event.logicalKey != LogicalKeyboardKey.arrowLeft &&
event.logicalKey != LogicalKeyboardKey.arrowRight) {
int _endOffsetOfNode(Node node) {
if (node is TextNode) {
return node.delta.length;
}
return 0;
}
extension on Position {
Position? goLeft(EditorState editorState) {
if (offset == 0) {
final node = editorState.document.nodeAtPath(path)!;
final prevNode = node.previous;
if (prevNode != null) {
return Position(
path: prevNode.path, offset: _endOffsetOfNode(prevNode));
}
return null;
}
return Position(path: path, offset: offset - 1);
}
Position? goRight(EditorState editorState) {
final node = editorState.document.nodeAtPath(path)!;
final lengthOfNode = _endOffsetOfNode(node);
if (offset >= lengthOfNode) {
final nextNode = node.next;
if (nextNode != null) {
return Position(path: nextNode.path, offset: 0);
}
return null;
}
return Position(path: path, offset: offset + 1);
}
}
Position? _goUp(EditorState editorState) {
final rects = editorState.service.selectionService.rects();
if (rects.isEmpty) {
return null;
}
final first = rects.first;
final firstOffset = Offset(first.left, first.top);
final hitOffset = firstOffset - Offset(0, first.height * 0.5);
return editorState.service.selectionService.hitTest(hitOffset);
}
Position? _goDown(EditorState editorState) {
final rects = editorState.service.selectionService.rects();
if (rects.isEmpty) {
return null;
}
final first = rects.last;
final firstOffset = Offset(first.right, first.bottom);
final hitOffset = firstOffset + Offset(0, first.height * 0.5);
return editorState.service.selectionService.hitTest(hitOffset);
}
KeyEventResult _handleShiftKey(EditorState editorState, RawKeyEvent event) {
final currentSelection = editorState.cursorSelection;
if (currentSelection == null) {
return KeyEventResult.ignored;
}
if (event.logicalKey == LogicalKeyboardKey.arrowLeft) {
final leftPosition = currentSelection.end.goLeft(editorState);
editorState.updateCursorSelection(leftPosition == null
? null
: Selection(start: currentSelection.start, end: leftPosition));
return KeyEventResult.handled;
} else if (event.logicalKey == LogicalKeyboardKey.arrowRight) {
final rightPosition = currentSelection.start.goRight(editorState);
editorState.updateCursorSelection(rightPosition == null
? null
: Selection(start: rightPosition, end: currentSelection.end));
return KeyEventResult.handled;
} else if (event.logicalKey == LogicalKeyboardKey.arrowUp) {
final position = _goUp(editorState);
editorState.updateCursorSelection(position == null
? null
: Selection(start: position, end: currentSelection.end));
return KeyEventResult.handled;
} else if (event.logicalKey == LogicalKeyboardKey.arrowDown) {
final position = _goDown(editorState);
editorState.updateCursorSelection(position == null
? null
: Selection(start: currentSelection.start, end: position));
return KeyEventResult.handled;
}
return KeyEventResult.ignored;
}
FlowyKeyEventHandler arrowKeysHandler = (editorState, event) {
if (event.isShiftPressed) {
return _handleShiftKey(editorState, event);
}
final currentSelection = editorState.cursorSelection;
if (currentSelection == null) {
return KeyEventResult.ignored;
}
if (event.logicalKey == LogicalKeyboardKey.arrowLeft) {
if (currentSelection.isCollapsed) {
final leftPosition = currentSelection.start.goLeft(editorState);
if (leftPosition != null) {
editorState.updateCursorSelection(Selection.collapsed(leftPosition));
}
} else {
editorState
.updateCursorSelection(currentSelection.collapse(atStart: true));
}
return KeyEventResult.handled;
} else if (event.logicalKey == LogicalKeyboardKey.arrowRight) {
if (currentSelection.isCollapsed) {
final rightPosition = currentSelection.end.goRight(editorState);
if (rightPosition != null) {
editorState.updateCursorSelection(Selection.collapsed(rightPosition));
}
} else {
editorState.updateCursorSelection(currentSelection.collapse());
}
return KeyEventResult.handled;
} else if (event.logicalKey == LogicalKeyboardKey.arrowUp) {
final position = _goUp(editorState);
editorState.updateCursorSelection(
position == null ? null : Selection.collapsed(position));
return KeyEventResult.handled;
} else if (event.logicalKey == LogicalKeyboardKey.arrowDown) {
final position = _goDown(editorState);
editorState.updateCursorSelection(
position == null ? null : Selection.collapsed(position));
return KeyEventResult.handled;
}
return KeyEventResult.ignored;
};

View File

@ -1,15 +1,17 @@
import 'package:flowy_editor/document/path.dart';
import 'dart:async';
import 'package:flowy_editor/document/node.dart';
import 'package:flowy_editor/document/position.dart';
import 'package:flowy_editor/document/selection.dart';
import 'package:flowy_editor/render/selection/selectable.dart';
import 'package:flowy_editor/render/selection/cursor_widget.dart';
import 'package:flowy_editor/render/selection/flowy_selection_widget.dart';
import 'package:flowy_editor/extensions/object_extensions.dart';
import 'package:flowy_editor/extensions/node_extensions.dart';
import 'package:flutter/gestures.dart';
import 'package:flowy_editor/service/shortcut_service.dart';
import 'package:flowy_editor/editor_state.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
/// Process selection and cursor
@ -28,6 +30,10 @@ mixin FlowySelectionService<T extends StatefulWidget> on State<T> {
///
void clearSelection();
List<Rect> rects();
Position? hitTest(Offset? offset);
///
List<Node> getNodesInSelection(Selection selection);
@ -50,7 +56,7 @@ mixin FlowySelectionService<T extends StatefulWidget> on State<T> {
/// [start] is the offset under the global coordinate system.
Node? computeNodeInOffset(Node node, Offset offset);
/// Return the [Node]s in multiple selection. Emtpy list would be returned
/// Return the [Node]s in multiple selection. Empty list would be returned
/// if no nodes are in range.
///
/// [start] is the offset under the global coordinate system.
@ -95,6 +101,92 @@ class FlowySelection extends StatefulWidget {
State<FlowySelection> createState() => _FlowySelectionState();
}
/// Because the flutter's [DoubleTapGestureRecognizer] will block the [TapGestureRecognizer]
/// for a while. So we need to implement our own GestureDetector.
@immutable
class _SelectionGestureDetector extends StatefulWidget {
const _SelectionGestureDetector(
{Key? key,
this.child,
this.onTapDown,
this.onDoubleTapDown,
this.onPanStart,
this.onPanUpdate,
this.onPanEnd})
: super(key: key);
@override
State<_SelectionGestureDetector> createState() =>
_SelectionGestureDetectorState();
final Widget? child;
final GestureTapDownCallback? onTapDown;
final GestureTapDownCallback? onDoubleTapDown;
final GestureDragStartCallback? onPanStart;
final GestureDragUpdateCallback? onPanUpdate;
final GestureDragEndCallback? onPanEnd;
}
class _SelectionGestureDetectorState extends State<_SelectionGestureDetector> {
bool _isDoubleTap = false;
Timer? _doubleTapTimer;
@override
Widget build(BuildContext context) {
return RawGestureDetector(
behavior: HitTestBehavior.translucent,
gestures: {
PanGestureRecognizer:
GestureRecognizerFactoryWithHandlers<PanGestureRecognizer>(
() => PanGestureRecognizer(),
(recognizer) {
recognizer
..onStart = widget.onPanStart
..onUpdate = widget.onPanUpdate
..onEnd = widget.onPanEnd;
},
),
TapGestureRecognizer:
GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
() => TapGestureRecognizer(),
(recognizer) {
recognizer.onTapDown = _tapDownDelegate;
},
),
},
child: widget.child,
);
}
_tapDownDelegate(TapDownDetails tapDownDetails) {
if (_isDoubleTap) {
_isDoubleTap = false;
_doubleTapTimer?.cancel();
_doubleTapTimer = null;
if (widget.onDoubleTapDown != null) {
widget.onDoubleTapDown!(tapDownDetails);
}
} else {
if (widget.onTapDown != null) {
widget.onTapDown!(tapDownDetails);
}
_isDoubleTap = true;
_doubleTapTimer?.cancel();
_doubleTapTimer = Timer(kDoubleTapTimeout, () {
_isDoubleTap = false;
_doubleTapTimer = null;
});
}
}
@override
void dispose() {
_doubleTapTimer?.cancel();
super.dispose();
}
}
class _FlowySelectionState extends State<FlowySelection>
with FlowySelectionService, WidgetsBindingObserver {
final _cursorKey = GlobalKey(debugLabel: 'cursor');
@ -110,6 +202,8 @@ class _FlowySelectionState extends State<FlowySelection>
/// Tap
Offset? tapOffset;
final List<Rect> _rects = [];
EditorState get editorState => widget.editorState;
@override
@ -146,33 +240,24 @@ class _FlowySelectionState extends State<FlowySelection>
@override
Widget build(BuildContext context) {
return RawGestureDetector(
behavior: HitTestBehavior.translucent,
gestures: {
PanGestureRecognizer:
GestureRecognizerFactoryWithHandlers<PanGestureRecognizer>(
() => PanGestureRecognizer(),
(recognizer) {
recognizer
..onStart = _onPanStart
..onUpdate = _onPanUpdate
..onEnd = _onPanEnd;
},
),
TapGestureRecognizer:
GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
() => TapGestureRecognizer(),
(recognizer) {
recognizer.onTapDown = _onTapDown;
},
),
},
return _SelectionGestureDetector(
onPanStart: _onPanStart,
onPanUpdate: _onPanUpdate,
onPanEnd: _onPanEnd,
onTapDown: _onTapDown,
onDoubleTapDown: _onDoubleTapDown,
child: widget.child,
);
}
@override
List<Rect> rects() {
return _rects;
}
@override
void updateSelection(Selection selection) {
_rects.clear();
_clearSelection();
// cursor
@ -267,6 +352,22 @@ class _FlowySelectionState extends State<FlowySelection>
return false;
}
void _onDoubleTapDown(TapDownDetails details) {
final offset = details.globalPosition;
final nodes = getNodesInRange(offset);
if (nodes.isEmpty) {
editorState.updateCursorSelection(null);
return;
}
final selectable = nodes.first.selectable;
if (selectable == null) {
editorState.updateCursorSelection(null);
return;
}
editorState
.updateCursorSelection(selectable.getWorldBoundaryInOffset(offset));
}
void _onTapDown(TapDownDetails details) {
// clear old state.
panStartOffset = null;
@ -274,16 +375,32 @@ class _FlowySelectionState extends State<FlowySelection>
tapOffset = details.globalPosition;
final nodes = getNodesInRange(tapOffset!);
if (nodes.isNotEmpty) {
assert(nodes.length == 1);
final selectable = nodes.first.selectable;
if (selectable != null) {
final position = selectable.getPositionInOffset(tapOffset!);
final selection = Selection.collapsed(position);
updateSelection(selection);
}
final position = hitTest(tapOffset);
if (position == null) {
return;
}
final selection = Selection.collapsed(position);
editorState.updateCursorSelection(selection);
}
@override
Position? hitTest(Offset? offset) {
if (offset == null) {
editorState.updateCursorSelection(null);
return null;
}
final nodes = getNodesInRange(offset);
if (nodes.isEmpty) {
editorState.updateCursorSelection(null);
return null;
}
assert(nodes.length == 1);
final selectable = nodes.first.selectable;
if (selectable == null) {
editorState.updateCursorSelection(null);
return null;
}
return selectable.getPositionInOffset(offset);
}
void _onPanStart(DragStartDetails details) {
@ -314,7 +431,7 @@ class _FlowySelectionState extends State<FlowySelection>
final selection = Selection(
start: isDownward ? start : end, end: isDownward ? end : start);
debugPrint('[_onPanUpdate] $selection');
updateSelection(selection);
editorState.updateCursorSelection(selection);
}
}
@ -385,6 +502,7 @@ class _FlowySelectionState extends State<FlowySelection>
final rects = selectable.getRectsInSelection(newSelection);
for (final rect in rects) {
_rects.add(_transformRectToGlobal(selectable, rect));
final overlay = OverlayEntry(
builder: ((context) => SelectionWidget(
color: widget.selectionColor,
@ -399,6 +517,11 @@ class _FlowySelectionState extends State<FlowySelection>
Overlay.of(context)?.insertAll(_selectionOverlays);
}
Rect _transformRectToGlobal(Selectable selectable, Rect r) {
final Offset topLeft = selectable.localToGlobal(Offset(r.left, r.top));
return Rect.fromLTWH(topLeft.dx, topLeft.dy, r.width, r.height);
}
void _updateCursor(Position position) {
final node = editorState.document.root.childAtPath(position.path);
@ -413,6 +536,7 @@ class _FlowySelectionState extends State<FlowySelection>
final selectable = node.selectable;
final rect = selectable?.getCursorRectInPosition(position);
if (rect != null) {
_rects.add(_transformRectToGlobal(selectable!, rect));
final cursor = OverlayEntry(
builder: ((context) => CursorWidget(
key: _cursorKey,
@ -423,9 +547,15 @@ class _FlowySelectionState extends State<FlowySelection>
);
_cursorOverlays.add(cursor);
Overlay.of(context)?.insertAll(_cursorOverlays);
_forceShowCursor();
}
}
_forceShowCursor() {
final currentState = _cursorKey.currentState as CursorWidgetState?;
currentState?.show();
}
List<Node> _selectedNodesInSelection(Node node, Selection selection) {
List<Node> result = [];
if (node.parent != null) {

View File

@ -7,7 +7,7 @@ import 'package:flowy_editor/operation/transaction.dart';
import 'package:flowy_editor/editor_state.dart';
import 'package:flutter/foundation.dart';
/// This class contains operations committed by users.
/// A [HistoryItem] contains list of 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> {

View File

@ -24,11 +24,11 @@ dev_dependencies:
# The following section is specific to Flutter packages.
flutter:
# To add assets to your package, add an assets section, like this:
assets:
- assets/images/uncheck.svg
- assets/images/
- assets/document.json
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
#

View File

@ -0,0 +1,83 @@
import 'dart:collection';
import 'package:flowy_editor/document/node.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flowy_editor/operation/operation.dart';
import 'package:flowy_editor/operation/transaction_builder.dart';
import 'package:flowy_editor/editor_state.dart';
import 'package:flowy_editor/document/state_tree.dart';
import 'package:flowy_editor/render/render_plugins.dart';
void main() {
group('transform path', () {
test('transform path changed', () {
expect(transformPath([0, 1], [0, 1]), [0, 2]);
expect(transformPath([0, 1], [0, 2]), [0, 3]);
expect(transformPath([0, 1], [0, 2, 7, 8, 9]), [0, 3, 7, 8, 9]);
expect(transformPath([0, 1, 2], [0, 0, 7, 8, 9]), [0, 0, 7, 8, 9]);
});
test("transform path not changed", () {
expect(transformPath([0, 1, 2], [0, 0, 7, 8, 9]), [0, 0, 7, 8, 9]);
expect(transformPath([0, 1, 2], [0, 1]), [0, 1]);
expect(transformPath([1, 1], [1, 0]), [1, 0]);
});
test("transform path delta", () {
expect(transformPath([0, 1], [0, 1], 5), [0, 6]);
});
});
group('transform operation', () {
test('insert + insert', () {
final t = transformOperation(
InsertOperation(path: [
0,
1
], value: Node(type: "node", attributes: {}, children: LinkedList())),
InsertOperation(
path: [0, 1],
value:
Node(type: "node", attributes: {}, children: LinkedList())));
expect(t.path, [0, 2]);
});
test('delete + delete', () {
final t = transformOperation(
DeleteOperation(
path: [0, 1],
removedValue:
Node(type: "node", attributes: {}, children: LinkedList())),
DeleteOperation(
path: [0, 2],
removedValue:
Node(type: "node", attributes: {}, children: LinkedList())));
expect(t.path, [0, 1]);
});
});
test('transform transaction builder', () {
final item1 = Node(type: "node", attributes: {}, children: LinkedList());
final item2 = Node(type: "node", attributes: {}, children: LinkedList());
final item3 = Node(type: "node", attributes: {}, children: LinkedList());
final root = Node(
type: "root",
attributes: {},
children: LinkedList()
..addAll([
item1,
item2,
item3,
]));
final state = EditorState(
document: StateTree(root: root), renderPlugins: RenderPlugins());
expect(item1.path, [0]);
expect(item2.path, [1]);
expect(item3.path, [2]);
final tb = TransactionBuilder(state);
tb.deleteNode(item1);
tb.deleteNode(item2);
tb.deleteNode(item3);
final transaction = tb.finish();
expect(transaction.operations[0].path, [0]);
expect(transaction.operations[1].path, [0]);
expect(transaction.operations[2].path, [0]);
});
}