mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
chore: resolve conflicts.
This commit is contained in:
commit
ec83a68602
10
frontend/.vscode/settings.json
vendored
10
frontend/.vscode/settings.json
vendored
@ -2,23 +2,21 @@
|
|||||||
"[dart]": {
|
"[dart]": {
|
||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
"editor.formatOnType": true,
|
"editor.formatOnType": true,
|
||||||
"editor.rulers": [
|
"editor.rulers": [80],
|
||||||
120
|
|
||||||
],
|
|
||||||
"editor.selectionHighlight": false,
|
"editor.selectionHighlight": false,
|
||||||
"editor.suggest.snippetsPreventQuickSuggestions": false,
|
"editor.suggest.snippetsPreventQuickSuggestions": false,
|
||||||
"editor.suggestSelection": "first",
|
"editor.suggestSelection": "first",
|
||||||
"editor.tabCompletion": "onlySnippets",
|
"editor.tabCompletion": "onlySnippets",
|
||||||
"editor.wordBasedSuggestions": false
|
"editor.wordBasedSuggestions": false,
|
||||||
},
|
},
|
||||||
"svgviewer.enableautopreview": true,
|
"svgviewer.enableautopreview": true,
|
||||||
"svgviewer.previewcolumn": "Active",
|
"svgviewer.previewcolumn": "Active",
|
||||||
"svgviewer.showzoominout": true,
|
"svgviewer.showzoominout": true,
|
||||||
"editor.wordWrapColumn": 120,
|
"editor.wordWrapColumn": 80,
|
||||||
"editor.minimap.maxColumn": 140,
|
"editor.minimap.maxColumn": 140,
|
||||||
"prettier.printWidth": 140,
|
"prettier.printWidth": 140,
|
||||||
"editor.wordWrap": "wordWrapColumn",
|
"editor.wordWrap": "wordWrapColumn",
|
||||||
"dart.lineLength": 120,
|
"dart.lineLength": 80,
|
||||||
"files.associations": {
|
"files.associations": {
|
||||||
"*.log.*": "log"
|
"*.log.*": "log"
|
||||||
},
|
},
|
||||||
|
45
frontend/app_flowy/packages/flowy_editor/.vscode/launch.json
vendored
Normal file
45
frontend/app_flowy/packages/flowy_editor/.vscode/launch.json
vendored
Normal 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"
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
@ -63,6 +63,11 @@ class __ImageNodeWidgetState extends State<_ImageNodeWidget> with Selectable {
|
|||||||
throw UnimplementedError();
|
throw UnimplementedError();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Offset localToGlobal(Offset offset) {
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Rect getCursorRectInPosition(Position position) {
|
Rect getCursorRectInPosition(Position position) {
|
||||||
// TODO: implement getCursorRectInPosition
|
// TODO: implement getCursorRectInPosition
|
||||||
|
@ -70,6 +70,11 @@ class _SelectedTextNodeWidgetState extends State<_SelectedTextNodeWidget>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Offset localToGlobal(Offset offset) {
|
||||||
|
return _renderParagraph.localToGlobal(offset);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Rect> getRectsInSelection(Selection selection) {
|
List<Rect> getRectsInSelection(Selection selection) {
|
||||||
assert(pathEquals(selection.start.path, selection.end.path));
|
assert(pathEquals(selection.start.path, selection.end.path));
|
||||||
|
@ -32,7 +32,11 @@ class Node extends ChangeNotifier with LinkedListEntry<Node> {
|
|||||||
required this.children,
|
required this.children,
|
||||||
required this.attributes,
|
required this.attributes,
|
||||||
this.parent,
|
this.parent,
|
||||||
});
|
}) {
|
||||||
|
for (final child in children) {
|
||||||
|
child.parent = this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
factory Node.fromJson(Map<String, Object> json) {
|
factory Node.fromJson(Map<String, Object> json) {
|
||||||
assert(json['type'] is String);
|
assert(json['type'] is String);
|
||||||
|
@ -38,7 +38,22 @@ class EditorState {
|
|||||||
final service = FlowyService();
|
final service = FlowyService();
|
||||||
|
|
||||||
final UndoManager undoManager = UndoManager();
|
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;
|
Timer? _debouncedSealHistoryItemTimer;
|
||||||
|
|
||||||
@ -74,7 +89,7 @@ class EditorState {
|
|||||||
for (final op in transaction.operations) {
|
for (final op in transaction.operations) {
|
||||||
_applyOperation(op);
|
_applyOperation(op);
|
||||||
}
|
}
|
||||||
cursorSelection = transaction.afterSelection;
|
updateCursorSelection(transaction.afterSelection);
|
||||||
|
|
||||||
if (options.recordUndo) {
|
if (options.recordUndo) {
|
||||||
final undoItem = undoManager.getUndoHistoryItem();
|
final undoItem = undoManager.getUndoHistoryItem();
|
||||||
|
@ -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/document/attributes.dart';
|
||||||
|
import 'package:flowy_editor/flowy_editor.dart';
|
||||||
|
|
||||||
abstract class Operation {
|
abstract class Operation {
|
||||||
|
final Path path;
|
||||||
|
Operation({required this.path});
|
||||||
|
Operation copyWithPath(Path path);
|
||||||
Operation invert();
|
Operation invert();
|
||||||
}
|
}
|
||||||
|
|
||||||
class InsertOperation extends Operation {
|
class InsertOperation extends Operation {
|
||||||
final Path path;
|
|
||||||
final Node value;
|
final Node value;
|
||||||
|
|
||||||
InsertOperation({
|
InsertOperation({
|
||||||
required this.path,
|
required super.path,
|
||||||
required this.value,
|
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
|
@override
|
||||||
Operation invert() {
|
Operation invert() {
|
||||||
return DeleteOperation(
|
return DeleteOperation(
|
||||||
@ -26,16 +32,25 @@ class InsertOperation extends Operation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class UpdateOperation extends Operation {
|
class UpdateOperation extends Operation {
|
||||||
final Path path;
|
|
||||||
final Attributes attributes;
|
final Attributes attributes;
|
||||||
final Attributes oldAttributes;
|
final Attributes oldAttributes;
|
||||||
|
|
||||||
UpdateOperation({
|
UpdateOperation({
|
||||||
required this.path,
|
required super.path,
|
||||||
required this.attributes,
|
required this.attributes,
|
||||||
required this.oldAttributes,
|
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
|
@override
|
||||||
Operation invert() {
|
Operation invert() {
|
||||||
return UpdateOperation(
|
return UpdateOperation(
|
||||||
@ -47,14 +62,19 @@ class UpdateOperation extends Operation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class DeleteOperation extends Operation {
|
class DeleteOperation extends Operation {
|
||||||
final Path path;
|
|
||||||
final Node removedValue;
|
final Node removedValue;
|
||||||
|
|
||||||
DeleteOperation({
|
DeleteOperation({
|
||||||
required this.path,
|
required super.path,
|
||||||
required this.removedValue,
|
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
|
@override
|
||||||
Operation invert() {
|
Operation invert() {
|
||||||
return InsertOperation(
|
return InsertOperation(
|
||||||
@ -65,18 +85,64 @@ class DeleteOperation extends Operation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class TextEditOperation extends Operation {
|
class TextEditOperation extends Operation {
|
||||||
final Path path;
|
|
||||||
final Delta delta;
|
final Delta delta;
|
||||||
final Delta inverted;
|
final Delta inverted;
|
||||||
|
|
||||||
TextEditOperation({
|
TextEditOperation({
|
||||||
required this.path,
|
required super.path,
|
||||||
required this.delta,
|
required this.delta,
|
||||||
required this.inverted,
|
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
|
@override
|
||||||
Operation invert() {
|
Operation invert() {
|
||||||
return TextEditOperation(path: path, delta: inverted, inverted: delta);
|
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;
|
||||||
|
}
|
||||||
|
@ -3,19 +3,15 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flowy_editor/document/selection.dart';
|
import 'package:flowy_editor/document/selection.dart';
|
||||||
import './operation.dart';
|
import './operation.dart';
|
||||||
|
|
||||||
/// This class to use to store the **changes**
|
/// A [Transaction] has a list of [Operation] objects that will be applied
|
||||||
/// will be applied to the editor.
|
/// to the editor. It is an immutable class and used to store and transmit.
|
||||||
///
|
///
|
||||||
/// This class is immutable version the the class
|
/// If you want to build a new [Transaction], use [TransactionBuilder] directly.
|
||||||
/// [[Transaction]]. Is used to stored and
|
|
||||||
/// transmit. If you want to build the transaction,
|
|
||||||
/// use [[Transaction]] directly.
|
|
||||||
///
|
///
|
||||||
/// There will be several ways to consume the transaction:
|
/// There will be several ways to consume the transaction:
|
||||||
/// 1. Apply to the state to update the UI.
|
/// 1. Apply to the state to update the UI.
|
||||||
/// 2. Send to the backend to store and do operation transforming.
|
/// 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
|
@immutable
|
||||||
class Transaction {
|
class Transaction {
|
||||||
final UnmodifiableListView<Operation> operations;
|
final UnmodifiableListView<Operation> operations;
|
||||||
|
@ -11,16 +11,10 @@ import 'package:flowy_editor/document/selection.dart';
|
|||||||
import './operation.dart';
|
import './operation.dart';
|
||||||
import './transaction.dart';
|
import './transaction.dart';
|
||||||
|
|
||||||
///
|
/// A [TransactionBuilder] is used to build the transaction from the state.
|
||||||
/// This class is used to
|
/// It will save make a snapshot of the cursor selection state automatically.
|
||||||
/// build the transaction from the state.
|
/// The cursor can be resoted if the transaction is undo.
|
||||||
///
|
|
||||||
/// This class automatically save the
|
|
||||||
/// cursor from the state.
|
|
||||||
///
|
|
||||||
/// When the transaction is undo, the
|
|
||||||
/// cursor can be restored.
|
|
||||||
///
|
|
||||||
class TransactionBuilder {
|
class TransactionBuilder {
|
||||||
final List<Operation> operations = [];
|
final List<Operation> operations = [];
|
||||||
EditorState state;
|
EditorState state;
|
||||||
@ -96,6 +90,9 @@ class TransactionBuilder {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for (var i = 0; i < operations.length; i++) {
|
||||||
|
op = transformOperation(operations[i], op);
|
||||||
|
}
|
||||||
operations.add(op);
|
operations.add(op);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,6 +22,11 @@ mixin DefaultSelectable {
|
|||||||
Selection getSelectionInRange(Offset start, Offset end) =>
|
Selection getSelectionInRange(Offset start, Offset end) =>
|
||||||
forward.getSelectionInRange(start, end);
|
forward.getSelectionInRange(start, end);
|
||||||
|
|
||||||
|
Offset localToGlobal(Offset offset) => forward.localToGlobal(offset);
|
||||||
|
|
||||||
|
Selection? getWorldBoundaryInOffset(Offset offset) =>
|
||||||
|
forward.getWorldBoundaryInOffset(offset);
|
||||||
|
|
||||||
Position start() => forward.start();
|
Position start() => forward.start();
|
||||||
|
|
||||||
Position end() => forward.end();
|
Position end() => forward.end();
|
||||||
|
@ -92,6 +92,16 @@ class _FlowyRichTextState extends State<FlowyRichText> with Selectable {
|
|||||||
return Position(path: widget.textNode.path, offset: baseOffset);
|
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
|
@override
|
||||||
List<Rect> getRectsInSelection(Selection selection) {
|
List<Rect> getRectsInSelection(Selection selection) {
|
||||||
assert(pathEquals(selection.start.path, selection.end.path) &&
|
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(
|
TextSpan get _textSpan => TextSpan(
|
||||||
children: widget.textNode.delta.operations
|
children: widget.textNode.delta.operations
|
||||||
.whereType<TextInsert>()
|
.whereType<TextInsert>()
|
||||||
|
@ -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/node_widget_builder.dart';
|
||||||
import 'package:flowy_editor/render/rich_text/default_selectable.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/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:flowy_editor/render/selection/selectable.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
@ -17,10 +17,10 @@ class CursorWidget extends StatefulWidget {
|
|||||||
final LayerLink layerLink;
|
final LayerLink layerLink;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<CursorWidget> createState() => _CursorWidgetState();
|
State<CursorWidget> createState() => CursorWidgetState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _CursorWidgetState extends State<CursorWidget> {
|
class CursorWidgetState extends State<CursorWidget> {
|
||||||
bool showCursor = true;
|
bool showCursor = true;
|
||||||
late Timer timer;
|
late Timer timer;
|
||||||
|
|
||||||
@ -28,7 +28,17 @@ class _CursorWidgetState extends State<CursorWidget> {
|
|||||||
void initState() {
|
void initState() {
|
||||||
super.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()),
|
Duration(milliseconds: (widget.blinkingInterval * 1000).toInt()),
|
||||||
(timer) {
|
(timer) {
|
||||||
setState(() {
|
setState(() {
|
||||||
@ -37,10 +47,13 @@ class _CursorWidgetState extends State<CursorWidget> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
/// force the cursor widget to show for a while
|
||||||
void dispose() {
|
show() {
|
||||||
|
setState(() {
|
||||||
|
showCursor = true;
|
||||||
|
});
|
||||||
timer.cancel();
|
timer.cancel();
|
||||||
super.dispose();
|
timer = _initTimer();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -21,8 +21,14 @@ mixin Selectable<T extends StatefulWidget> on State<T> {
|
|||||||
///
|
///
|
||||||
/// The return result must be an offset of the local coordinate system.
|
/// The return result must be an offset of the local coordinate system.
|
||||||
Position getPositionInOffset(Offset start);
|
Position getPositionInOffset(Offset start);
|
||||||
|
Selection? getWorldBoundaryInOffset(Offset start) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
Rect getCursorRectInPosition(Position position);
|
Rect getCursorRectInPosition(Position position);
|
||||||
|
|
||||||
|
Offset localToGlobal(Offset offset);
|
||||||
|
|
||||||
Position start();
|
Position start();
|
||||||
Position end();
|
Position end();
|
||||||
|
|
||||||
|
@ -1,14 +1,143 @@
|
|||||||
|
import 'package:flowy_editor/flowy_editor.dart';
|
||||||
import 'package:flowy_editor/service/keyboard_service.dart';
|
import 'package:flowy_editor/service/keyboard_service.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
FlowyKeyEventHandler arrowKeysHandler = (editorState, event) {
|
int _endOffsetOfNode(Node node) {
|
||||||
if (event.logicalKey != LogicalKeyboardKey.arrowUp &&
|
if (node is TextNode) {
|
||||||
event.logicalKey != LogicalKeyboardKey.arrowDown &&
|
return node.delta.length;
|
||||||
event.logicalKey != LogicalKeyboardKey.arrowLeft &&
|
}
|
||||||
event.logicalKey != LogicalKeyboardKey.arrowRight) {
|
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;
|
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;
|
return KeyEventResult.ignored;
|
||||||
};
|
};
|
||||||
|
@ -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/node.dart';
|
||||||
import 'package:flowy_editor/document/position.dart';
|
import 'package:flowy_editor/document/position.dart';
|
||||||
import 'package:flowy_editor/document/selection.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/cursor_widget.dart';
|
||||||
import 'package:flowy_editor/render/selection/flowy_selection_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/object_extensions.dart';
|
||||||
import 'package:flowy_editor/extensions/node_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/service/shortcut_service.dart';
|
||||||
import 'package:flowy_editor/editor_state.dart';
|
import 'package:flowy_editor/editor_state.dart';
|
||||||
|
|
||||||
import 'package:flutter/gestures.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
/// Process selection and cursor
|
/// Process selection and cursor
|
||||||
@ -28,6 +30,10 @@ mixin FlowySelectionService<T extends StatefulWidget> on State<T> {
|
|||||||
///
|
///
|
||||||
void clearSelection();
|
void clearSelection();
|
||||||
|
|
||||||
|
List<Rect> rects();
|
||||||
|
|
||||||
|
Position? hitTest(Offset? offset);
|
||||||
|
|
||||||
///
|
///
|
||||||
List<Node> getNodesInSelection(Selection selection);
|
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.
|
/// [start] is the offset under the global coordinate system.
|
||||||
Node? computeNodeInOffset(Node node, Offset offset);
|
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.
|
/// if no nodes are in range.
|
||||||
///
|
///
|
||||||
/// [start] is the offset under the global coordinate system.
|
/// [start] is the offset under the global coordinate system.
|
||||||
@ -95,6 +101,92 @@ class FlowySelection extends StatefulWidget {
|
|||||||
State<FlowySelection> createState() => _FlowySelectionState();
|
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>
|
class _FlowySelectionState extends State<FlowySelection>
|
||||||
with FlowySelectionService, WidgetsBindingObserver {
|
with FlowySelectionService, WidgetsBindingObserver {
|
||||||
final _cursorKey = GlobalKey(debugLabel: 'cursor');
|
final _cursorKey = GlobalKey(debugLabel: 'cursor');
|
||||||
@ -110,6 +202,8 @@ class _FlowySelectionState extends State<FlowySelection>
|
|||||||
/// Tap
|
/// Tap
|
||||||
Offset? tapOffset;
|
Offset? tapOffset;
|
||||||
|
|
||||||
|
final List<Rect> _rects = [];
|
||||||
|
|
||||||
EditorState get editorState => widget.editorState;
|
EditorState get editorState => widget.editorState;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -146,33 +240,24 @@ class _FlowySelectionState extends State<FlowySelection>
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return RawGestureDetector(
|
return _SelectionGestureDetector(
|
||||||
behavior: HitTestBehavior.translucent,
|
onPanStart: _onPanStart,
|
||||||
gestures: {
|
onPanUpdate: _onPanUpdate,
|
||||||
PanGestureRecognizer:
|
onPanEnd: _onPanEnd,
|
||||||
GestureRecognizerFactoryWithHandlers<PanGestureRecognizer>(
|
onTapDown: _onTapDown,
|
||||||
() => PanGestureRecognizer(),
|
onDoubleTapDown: _onDoubleTapDown,
|
||||||
(recognizer) {
|
|
||||||
recognizer
|
|
||||||
..onStart = _onPanStart
|
|
||||||
..onUpdate = _onPanUpdate
|
|
||||||
..onEnd = _onPanEnd;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
TapGestureRecognizer:
|
|
||||||
GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
|
|
||||||
() => TapGestureRecognizer(),
|
|
||||||
(recognizer) {
|
|
||||||
recognizer.onTapDown = _onTapDown;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
},
|
|
||||||
child: widget.child,
|
child: widget.child,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Rect> rects() {
|
||||||
|
return _rects;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void updateSelection(Selection selection) {
|
void updateSelection(Selection selection) {
|
||||||
|
_rects.clear();
|
||||||
_clearSelection();
|
_clearSelection();
|
||||||
|
|
||||||
// cursor
|
// cursor
|
||||||
@ -267,6 +352,22 @@ class _FlowySelectionState extends State<FlowySelection>
|
|||||||
return false;
|
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) {
|
void _onTapDown(TapDownDetails details) {
|
||||||
// clear old state.
|
// clear old state.
|
||||||
panStartOffset = null;
|
panStartOffset = null;
|
||||||
@ -274,16 +375,32 @@ class _FlowySelectionState extends State<FlowySelection>
|
|||||||
|
|
||||||
tapOffset = details.globalPosition;
|
tapOffset = details.globalPosition;
|
||||||
|
|
||||||
final nodes = getNodesInRange(tapOffset!);
|
final position = hitTest(tapOffset);
|
||||||
if (nodes.isNotEmpty) {
|
if (position == null) {
|
||||||
assert(nodes.length == 1);
|
return;
|
||||||
final selectable = nodes.first.selectable;
|
|
||||||
if (selectable != null) {
|
|
||||||
final position = selectable.getPositionInOffset(tapOffset!);
|
|
||||||
final selection = Selection.collapsed(position);
|
|
||||||
updateSelection(selection);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
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) {
|
void _onPanStart(DragStartDetails details) {
|
||||||
@ -314,7 +431,7 @@ class _FlowySelectionState extends State<FlowySelection>
|
|||||||
final selection = Selection(
|
final selection = Selection(
|
||||||
start: isDownward ? start : end, end: isDownward ? end : start);
|
start: isDownward ? start : end, end: isDownward ? end : start);
|
||||||
debugPrint('[_onPanUpdate] $selection');
|
debugPrint('[_onPanUpdate] $selection');
|
||||||
updateSelection(selection);
|
editorState.updateCursorSelection(selection);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -385,6 +502,7 @@ class _FlowySelectionState extends State<FlowySelection>
|
|||||||
final rects = selectable.getRectsInSelection(newSelection);
|
final rects = selectable.getRectsInSelection(newSelection);
|
||||||
|
|
||||||
for (final rect in rects) {
|
for (final rect in rects) {
|
||||||
|
_rects.add(_transformRectToGlobal(selectable, rect));
|
||||||
final overlay = OverlayEntry(
|
final overlay = OverlayEntry(
|
||||||
builder: ((context) => SelectionWidget(
|
builder: ((context) => SelectionWidget(
|
||||||
color: widget.selectionColor,
|
color: widget.selectionColor,
|
||||||
@ -399,6 +517,11 @@ class _FlowySelectionState extends State<FlowySelection>
|
|||||||
Overlay.of(context)?.insertAll(_selectionOverlays);
|
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) {
|
void _updateCursor(Position position) {
|
||||||
final node = editorState.document.root.childAtPath(position.path);
|
final node = editorState.document.root.childAtPath(position.path);
|
||||||
|
|
||||||
@ -413,6 +536,7 @@ class _FlowySelectionState extends State<FlowySelection>
|
|||||||
final selectable = node.selectable;
|
final selectable = node.selectable;
|
||||||
final rect = selectable?.getCursorRectInPosition(position);
|
final rect = selectable?.getCursorRectInPosition(position);
|
||||||
if (rect != null) {
|
if (rect != null) {
|
||||||
|
_rects.add(_transformRectToGlobal(selectable!, rect));
|
||||||
final cursor = OverlayEntry(
|
final cursor = OverlayEntry(
|
||||||
builder: ((context) => CursorWidget(
|
builder: ((context) => CursorWidget(
|
||||||
key: _cursorKey,
|
key: _cursorKey,
|
||||||
@ -423,9 +547,15 @@ class _FlowySelectionState extends State<FlowySelection>
|
|||||||
);
|
);
|
||||||
_cursorOverlays.add(cursor);
|
_cursorOverlays.add(cursor);
|
||||||
Overlay.of(context)?.insertAll(_cursorOverlays);
|
Overlay.of(context)?.insertAll(_cursorOverlays);
|
||||||
|
_forceShowCursor();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_forceShowCursor() {
|
||||||
|
final currentState = _cursorKey.currentState as CursorWidgetState?;
|
||||||
|
currentState?.show();
|
||||||
|
}
|
||||||
|
|
||||||
List<Node> _selectedNodesInSelection(Node node, Selection selection) {
|
List<Node> _selectedNodesInSelection(Node node, Selection selection) {
|
||||||
List<Node> result = [];
|
List<Node> result = [];
|
||||||
if (node.parent != null) {
|
if (node.parent != null) {
|
||||||
|
@ -7,7 +7,7 @@ import 'package:flowy_editor/operation/transaction.dart';
|
|||||||
import 'package:flowy_editor/editor_state.dart';
|
import 'package:flowy_editor/editor_state.dart';
|
||||||
import 'package:flutter/foundation.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.
|
/// If a [HistoryItem] is not sealed, operations can be added sequentially.
|
||||||
/// Otherwise, the operations should be added to a new [HistoryItem].
|
/// Otherwise, the operations should be added to a new [HistoryItem].
|
||||||
class HistoryItem extends LinkedListEntry<HistoryItem> {
|
class HistoryItem extends LinkedListEntry<HistoryItem> {
|
||||||
|
@ -24,11 +24,11 @@ dev_dependencies:
|
|||||||
|
|
||||||
# The following section is specific to Flutter packages.
|
# The following section is specific to Flutter packages.
|
||||||
flutter:
|
flutter:
|
||||||
|
|
||||||
# To add assets to your package, add an assets section, like this:
|
# To add assets to your package, add an assets section, like this:
|
||||||
assets:
|
assets:
|
||||||
- assets/images/uncheck.svg
|
- assets/images/uncheck.svg
|
||||||
- assets/images/
|
- assets/images/
|
||||||
|
- assets/document.json
|
||||||
# - images/a_dot_burr.jpeg
|
# - images/a_dot_burr.jpeg
|
||||||
# - images/a_dot_ham.jpeg
|
# - images/a_dot_ham.jpeg
|
||||||
#
|
#
|
||||||
|
@ -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]);
|
||||||
|
});
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user