Browse Source

add redo && undo func

lucky1213 4 years ago
parent
commit
c50f11cba0

+ 4
- 0
packages/notus/CHANGELOG.md View File

@@ -1,3 +1,7 @@
1
+## 0.1.6
2
+
3
+* redo && undo func
4
+
1 5
 ## 0.1.5
2 6
 
3 7
 * Bumped minimum Dart SDK version to 2.2

+ 24
- 1
packages/notus/lib/src/document.dart View File

@@ -11,6 +11,7 @@ import 'document/leaf.dart';
11 11
 import 'document/line.dart';
12 12
 import 'document/node.dart';
13 13
 import 'heuristics.dart';
14
+import 'history.dart';
14 15
 
15 16
 /// Source of a [NotusChange].
16 17
 enum ChangeSource {
@@ -40,12 +41,15 @@ class NotusDocument {
40 41
   /// Creates new empty Notus document.
41 42
   NotusDocument()
42 43
       : _heuristics = NotusHeuristics.fallback,
44
+        // _delta = Delta()..insert('\n') {
45
+      _history = NotusHistory(),
43 46
         _delta = Delta()..insert('\n') {
44 47
     _loadDocument(_delta);
45 48
   }
46 49
 
47 50
   NotusDocument.fromJson(List data)
48 51
       : _heuristics = NotusHeuristics.fallback,
52
+      _history = NotusHistory(),
49 53
         _delta = Delta.fromJson(data) {
50 54
     _loadDocument(_delta);
51 55
   }
@@ -59,6 +63,15 @@ class NotusDocument {
59 63
 
60 64
   final NotusHeuristics _heuristics;
61 65
 
66
+  NotusHistory _history;
67
+
68
+  set history(NotusHistory value) {
69
+    _history?.clear();
70
+    _history = value;
71
+  }
72
+
73
+  NotusHistory get history => _history;
74
+
62 75
   /// The root node of this document tree.
63 76
   RootNode get root => _root;
64 77
   final RootNode _root = RootNode();
@@ -216,7 +229,7 @@ class NotusDocument {
216 229
   /// of this document.
217 230
   ///
218 231
   /// In case the [change] is invalid, behavior of this method is unspecified.
219
-  void compose(Delta change, ChangeSource source) {
232
+  void compose(Delta change, ChangeSource source, {bool history}) {
220 233
     _checkMutable();
221 234
     change.trim();
222 235
     assert(change.isNotEmpty);
@@ -293,4 +306,14 @@ class NotusDocument {
293 306
       _root.remove(node);
294 307
     }
295 308
   }
309
+
310
+  void undo() {
311
+    assert(_history != null);
312
+    _history?.undo(this);
313
+  }
314
+
315
+  void redo() {
316
+    assert(_history != null);
317
+    _history?.redo(this);
318
+  }
296 319
 }

+ 119
- 0
packages/notus/lib/src/history.dart View File

@@ -0,0 +1,119 @@
1
+import 'package:notus/notus.dart';
2
+import 'package:notus/src/document.dart';
3
+import 'package:quill_delta/quill_delta.dart';
4
+
5
+///
6
+/// record users operation or api change(Collaborative editing)
7
+/// used for redo or undo function
8
+///
9
+class NotusHistory {
10
+  final NotusHistoryStack stack = NotusHistoryStack.empty();
11
+
12
+  /// used for disable redo or undo function
13
+  bool ignoreChange;
14
+
15
+  int lastRecorded;
16
+
17
+  ///Collaborative editing's conditions should be true
18
+  final bool userOnly;
19
+
20
+  ///max operation count for undo
21
+  final int maxStack;
22
+
23
+  ///record delay
24
+  final int interval;
25
+
26
+  NotusHistory(
27
+      {this.ignoreChange = false,
28
+      this.interval = 400,
29
+      this.maxStack = 100,
30
+      this.userOnly = false,
31
+      this.lastRecorded = 0});
32
+
33
+  void handleDocChange(NotusChange event) {
34
+    if (ignoreChange) return;
35
+    if (!userOnly || event.source == ChangeSource.local) {
36
+      record(event.change, event.before);
37
+    } else {
38
+      transform(event.change);
39
+    }
40
+  }
41
+
42
+  void clear() {
43
+    stack.clear();
44
+  }
45
+
46
+  void record(Delta change, Delta before) {
47
+    if (change.isEmpty) return;
48
+    stack.redo.clear();
49
+    Delta undoDelta = change.invert(before);
50
+    final timeStamp = DateTime.now().millisecondsSinceEpoch;
51
+
52
+    if (lastRecorded + interval > timeStamp && stack.undo.isNotEmpty) {
53
+      final lastDelta = stack.undo.removeLast();
54
+      undoDelta = undoDelta.compose(lastDelta);
55
+    } else {
56
+      lastRecorded = timeStamp;
57
+    }
58
+
59
+    if (undoDelta.isEmpty) return;
60
+    stack.undo.add(undoDelta);
61
+
62
+    if (stack.undo.length > maxStack) {
63
+      stack.undo.removeAt(0);
64
+    }
65
+  }
66
+
67
+  ///
68
+  ///It will override pre local undo delta,replaced by remote change
69
+  ///
70
+  void transform(Delta delta) {
71
+    transformStack(this.stack.undo, delta);
72
+    transformStack(this.stack.redo, delta);
73
+  }
74
+
75
+  void transformStack(List<Delta> stack, Delta delta) {
76
+    for (int i = stack.length - 1; i >= 0; i -= 1) {
77
+      final oldDelta = stack[i];
78
+      stack[i] = delta.transform(oldDelta, true);
79
+      delta = oldDelta.transform(delta, false);
80
+      if (stack[i].length == 0) {
81
+        stack.removeAt(i);
82
+      }
83
+    }
84
+  }
85
+
86
+  void _change(NotusDocument doc, List<Delta> source, List<Delta> dest) {
87
+    if (source.length == 0) return;
88
+    Delta delta = source.removeLast();
89
+    Delta base = doc.toDelta();
90
+    Delta inverseDelta = delta.invert(base);
91
+    dest.add(inverseDelta);
92
+    this.lastRecorded = 0;
93
+    this.ignoreChange = true;
94
+    doc.compose(delta, ChangeSource.local, history: true);
95
+    this.ignoreChange = false;
96
+  }
97
+
98
+  void undo(NotusDocument doc) {
99
+    _change(doc, stack.undo, stack.redo);
100
+  }
101
+
102
+  void redo(NotusDocument doc) {
103
+    _change(doc, stack.redo, stack.undo);
104
+  }
105
+}
106
+
107
+class NotusHistoryStack {
108
+  final List<Delta> undo;
109
+  final List<Delta> redo;
110
+
111
+  NotusHistoryStack.empty()
112
+      : undo = [],
113
+        redo = [];
114
+
115
+  void clear() {
116
+    undo.clear();
117
+    redo.clear();
118
+  }
119
+}

+ 1
- 1
packages/notus/pubspec.yaml View File

@@ -1,6 +1,6 @@
1 1
 name: notus
2 2
 description: Platform-agnostic rich text document model based on Delta format and used in Zefyr editor.
3
-version: 0.1.5
3
+version: 0.1.6
4 4
 author: Anatoly Pulyaevskiy <anatoly.pulyaevskiy@gmail.com>
5 5
 homepage: https://github.com/memspace/zefyr
6 6
 

+ 5
- 0
packages/zefyr/example/lib/src/images.dart View File

@@ -53,6 +53,11 @@ class CustomImageDelegate implements ZefyrImageDelegate<ImageSource> {
53 53
     }
54 54
   }
55 55
 
56
+  @override
57
+  void onImageTap(BuildContext context, String key) {
58
+    print(key);
59
+  }
60
+
56 61
   @override
57 62
   Future<String> picked(file, isFullImage) async {
58 63
     print(isFullImage);

+ 10
- 0
packages/zefyr/lib/src/widgets/controller.dart View File

@@ -207,6 +207,16 @@ class ZefyrController extends ChangeNotifier {
207 207
     formatText(index, length, attribute);
208 208
   }
209 209
 
210
+  void undo() {
211
+    document.undo();
212
+    updateSelection(TextSelection.collapsed(offset: document.length));
213
+  }
214
+
215
+  void redo() {
216
+    document.redo();
217
+    updateSelection(TextSelection.collapsed(offset: document.length));
218
+  }
219
+  
210 220
   /// Returns style of specified text range.
211 221
   ///
212 222
   /// If nothing is selected but we've toggled an attribute,

+ 8
- 1
packages/zefyr/lib/src/widgets/image.dart View File

@@ -30,6 +30,8 @@ abstract class ZefyrImageDelegate<S> {
30 30
   /// [pickImage] method.
31 31
   Widget buildImage(BuildContext context, String key);
32 32
 
33
+  void onImageTap(BuildContext context, String key);
34
+
33 35
   /// Picks an image from specified [source].
34 36
   ///
35 37
   /// Returns unique string key for the selected image. Returned key is stored
@@ -67,7 +69,12 @@ class _ZefyrImageState extends State<ZefyrImage> {
67 69
     return _EditableImage(
68 70
       child: Padding(
69 71
         padding: theme.defaultLineTheme.padding,
70
-        child: image,
72
+        child: GestureDetector(
73
+          onTap: () {
74
+            widget.delegate.onImageTap(context, imageSource);
75
+          },
76
+          child: image,
77
+        ),
71 78
       ),
72 79
       node: widget.node,
73 80
     );

+ 12
- 0
packages/zefyr/lib/src/widgets/scope.dart View File

@@ -252,6 +252,18 @@ class ZefyrScope extends ChangeNotifier {
252 252
     _controller.formatSelection(value);
253 253
   }
254 254
 
255
+  void undo() {
256
+    assert(isEditable);
257
+    assert(!_disposed);
258
+    _controller.undo();
259
+  }
260
+
261
+  void redo() {
262
+    assert(isEditable);
263
+    assert(!_disposed);
264
+    _controller.redo();
265
+  }
266
+
255 267
   void focus() {
256 268
     assert(isEditable);
257 269
     assert(!_disposed);

+ 4
- 0
packages/zefyr/test/widgets/buttons_test.dart View File

@@ -188,6 +188,10 @@ class _TestImageDelegate implements ZefyrImageDelegate<String> {
188 188
   Widget buildImage(BuildContext context, String key) {
189 189
     return Image.file(File(key));
190 190
   }
191
+  @override
192
+  void onImageTap(BuildContext context, String key) {
193
+    print(key);
194
+  }
191 195
 
192 196
   @override
193 197
   String get cameraSource => "camera";

+ 4
- 0
packages/zefyr/test/widgets/image_test.dart View File

@@ -92,6 +92,10 @@ class _TestImageDelegate implements ZefyrImageDelegate<String> {
92 92
   Widget buildImage(BuildContext context, String key) {
93 93
     return Image.file(File(key));
94 94
   }
95
+  @override
96
+  void onImageTap(BuildContext context, String key) {
97
+    print(key);
98
+  }
95 99
 
96 100
   @override
97 101
   String get cameraSource => "camera";

+ 4
- 0
packages/zefyr/test/widgets/scope_test.dart View File

@@ -79,6 +79,10 @@ class _TestImageDelegate implements ZefyrImageDelegate<String> {
79 79
   Widget buildImage(BuildContext context, String key) {
80 80
     return null;
81 81
   }
82
+  @override
83
+  void onImageTap(BuildContext context, String key) {
84
+    print(key);
85
+  }
82 86
 
83 87
   @override
84 88
   String get cameraSource => null;