Pārlūkot izejas kodu

feat: history-undo-redo (unstable)

Raihan Rizal 3 mēneši atpakaļ
vecāks
revīzija
02d81a56cf

+ 1 - 1
lib/canvas_setup_page.dart

@@ -81,7 +81,7 @@ class _CanvasSetupPageState extends State<CanvasSetupPage> {
81 81
                       context: context, 
82 82
                       builder: (context) => AlertDialog(
83 83
                         title: Text('Update Label Size'),
84
-                        content: Text('updating to smaller label size may cause element lost, Are you sure want to update label size ?'),
84
+                        content: Text('updating label size will remove all history you have changed, Are you sure want to update label size ?'),
85 85
                         actions: [
86 86
                           TextButton(
87 87
                             onPressed: () => Navigator.pop(context, true), 

+ 13 - 0
lib/history.dart

@@ -0,0 +1,13 @@
1
+
2
+import 'package:flutter_canvas_editor/widgets/elements.dart';
3
+
4
+enum CanvasHistoryModifyType { add, remove, move, resize, lock, rotate, textEdit, redo}
5
+
6
+class CanvasHistory {
7
+  final CanvasHistoryModifyType type;
8
+  List<ElementProperty> elementPropeties;
9
+
10
+  CanvasHistory(this.type, this.elementPropeties);
11
+
12
+  List<ElementProperty> get getState => elementPropeties;
13
+}

+ 14 - 13
lib/main.dart

@@ -3,6 +3,7 @@ import 'dart:math';
3 3
 import 'package:defer_pointer/defer_pointer.dart';
4 4
 import 'package:flutter/material.dart';
5 5
 import 'package:flutter_canvas_editor/canvas_setup_page.dart';
6
+import 'package:flutter_canvas_editor/history.dart';
6 7
 import 'package:flutter_canvas_editor/providers/editor.dart';
7 8
 import 'package:flutter_canvas_editor/snaptest_page.dart';
8 9
 import 'package:flutter_canvas_editor/test_page.dart';
@@ -81,19 +82,6 @@ class _HomePageState extends State<HomePage> {
81 82
   // functions
82 83
   void setInitialZoom() {
83 84
     Provider.of<Editor>(context, listen: false).setCanvasTransformationInitialZoom(context);
84
-
85
-    // double deviceWidth = MediaQuery.of(context).size.width;
86
-    // double deviceHeight = MediaQuery.of(context).size.height;
87
-
88
-    // print('device width: $deviceWidth');
89
-    // print('device height: $deviceHeight');
90
-
91
-    // initialScale = deviceWidth / Provider.of<Editor>(context, listen: false).canvasProperty.width * 0.9;
92
-    
93
-    // print('initialScale $initialScale');
94
-
95
-    // _transformationController.value = Matrix4.identity()..scale(initialScale);
96
-    
97 85
   }
98 86
 
99 87
   void setTransformControllerListener() {
@@ -118,6 +106,19 @@ class _HomePageState extends State<HomePage> {
118 106
           title: Text('Template Editor'),
119 107
           actions: [
120 108
 
109
+            IconButton(
110
+              onPressed: Provider.of<Editor>(context, listen: false).undoStack.isEmpty ? null : () {
111
+                Provider.of<Editor>(context, listen: false).undo();
112
+              },
113
+              icon: Icon(Icons.undo)
114
+            ),
115
+            IconButton(
116
+              onPressed: Provider.of<Editor>(context, listen: false).redoStack.isEmpty ? null : () {
117
+                Provider.of<Editor>(context, listen: false).redo();
118
+              },
119
+              icon: Icon(Icons.redo)
120
+            ),
121
+
121 122
             // ! DEBUG BUTTON
122 123
             IconButton(
123 124
               onPressed: () {

+ 122 - 2
lib/providers/editor.dart

@@ -3,10 +3,12 @@ import 'dart:developer';
3 3
 
4 4
 import 'package:defer_pointer/defer_pointer.dart';
5 5
 import 'package:flutter/material.dart';
6
+import 'package:flutter_canvas_editor/history.dart';
6 7
 import 'package:flutter_canvas_editor/style/canvas_style.dart';
7 8
 import 'package:flutter_canvas_editor/widgets/elements.dart';
8 9
 import 'package:toastification/toastification.dart';
9 10
 import 'package:uuid/uuid.dart';
11
+import 'package:collection/collection.dart';
10 12
 
11 13
 import '../main.dart';
12 14
 
@@ -65,12 +67,91 @@ class Editor extends ChangeNotifier {
65 67
       qrScale: 3
66 68
     )
67 69
   ];
68
-  // int _reservedIdUntil = 0;
70
+
71
+
72
+
73
+
74
+  // ? History stack
75
+  List<CanvasHistory> undoStack = [];
76
+  List<List<ElementProperty>> redoStack = [];
77
+
78
+
79
+  void addUndoEntry(CanvasHistoryModifyType canvasHistoryType, List<ElementProperty> elementProperties) {
80
+    undoStack.add(CanvasHistory(canvasHistoryType, _cloneCurrentState(elementProperties)));
81
+
82
+    redoStack.clear();
83
+
84
+    notifyListeners();
85
+  }
86
+
87
+  void undo() {
88
+    addRedoEntry(elementProperties);
89
+
90
+    // apply to current state
91
+    if (undoStack.last.type == CanvasHistoryModifyType.textEdit && undoStack.last.getState != elementProperties) {
92
+      undoStack.removeLast();
93
+    }
94
+
95
+    elementProperties = undoStack.last.getState;
96
+
97
+    // unselect element
98
+    if (elementProperties.firstWhereOrNull((e) => e.id == selectedElmId) == null) {
99
+      unSelectElm();
100
+    }
101
+
102
+    undoStack.removeLast();
103
+
104
+    notifyListeners();
105
+  }
106
+
107
+
108
+  void addRedoEntry(List<ElementProperty> elementProperties) {
109
+    redoStack.add(_cloneCurrentState(elementProperties));
110
+
111
+    notifyListeners();
112
+  }
113
+
114
+  void redo() {
115
+    undoStack.add(CanvasHistory(CanvasHistoryModifyType.redo, _cloneCurrentState(elementProperties)));
116
+     
117
+    // apply to current state
118
+    elementProperties = redoStack.last;
119
+
120
+    redoStack.removeLast();
121
+
122
+    notifyListeners();
123
+  }
124
+
125
+  List<ElementProperty> _cloneCurrentState(List<ElementProperty> elementProperties) {
126
+    List<ElementProperty> clonedElementProperties = elementProperties.map((elementProperty) {
127
+      return ElementProperty(
128
+        id: elementProperty.id,
129
+        valueController: TextEditingController(text: elementProperty.valueController.text),
130
+        type: elementProperty.type,
131
+        position: ElementPosition(top: elementProperty.position.top, left: elementProperty.position.left),
132
+        width: elementProperty.width,
133
+        quarterTurns: elementProperty.quarterTurns,
134
+        elementKey: elementProperty.elementKey,
135
+        qrScale: elementProperty.qrScale,
136
+        fontScale: elementProperty.fontScale,
137
+        variableType: elementProperty.variableType,
138
+        isLocked: elementProperty.isLocked
139
+      );
140
+    }).toList();
141
+
142
+    return clonedElementProperties;
143
+  }
144
+
145
+
146
+
69 147
 
70 148
   // ? udpate canvas
71 149
   void updateCanvasProperty(BuildContext context, double width, double height) {
72 150
     canvasProperty.height = height;
73 151
     canvasProperty.width = width;
152
+    
153
+    undoStack.clear();
154
+    redoStack.clear();
74 155
 
75 156
     _adjustOutOfBoundElement();
76 157
 
@@ -122,6 +203,10 @@ class Editor extends ChangeNotifier {
122 203
     );
123 204
 
124 205
 
206
+    // Save History
207
+    addUndoEntry(CanvasHistoryModifyType.add, elementProperties);
208
+
209
+
125 210
     elementProperties.add(element);
126 211
     // _reservedIdUntil = elementProperties.length - 1;r
127 212
     notifyListeners();
@@ -142,6 +227,9 @@ class Editor extends ChangeNotifier {
142 227
       elementKey: GlobalKey()
143 228
     );
144 229
 
230
+    // Save History
231
+    addUndoEntry(CanvasHistoryModifyType.add, elementProperties);
232
+
145 233
     elementProperties.add(element);
146 234
     notifyListeners();
147 235
 
@@ -164,6 +252,9 @@ class Editor extends ChangeNotifier {
164 252
       elementKey: GlobalKey()
165 253
     );
166 254
 
255
+    // Save History
256
+    addUndoEntry(CanvasHistoryModifyType.add, elementProperties);
257
+
167 258
     elementProperties.add(element);
168 259
     notifyListeners();
169 260
 
@@ -184,6 +275,9 @@ class Editor extends ChangeNotifier {
184 275
       elementKey: GlobalKey()
185 276
     );
186 277
 
278
+    // Save History
279
+    addUndoEntry(CanvasHistoryModifyType.add, elementProperties);
280
+
187 281
     elementProperties.add(element);
188 282
     notifyListeners();
189 283
 
@@ -204,6 +298,9 @@ class Editor extends ChangeNotifier {
204 298
       elementKey: GlobalKey()
205 299
     );
206 300
 
301
+    // Save History
302
+    addUndoEntry(CanvasHistoryModifyType.add, elementProperties);
303
+
207 304
     elementProperties.add(element);
208 305
     notifyListeners();
209 306
 
@@ -224,6 +321,9 @@ class Editor extends ChangeNotifier {
224 321
       elementKey: GlobalKey()
225 322
     );
226 323
 
324
+    // Save History
325
+    addUndoEntry(CanvasHistoryModifyType.add, elementProperties);
326
+
227 327
     elementProperties.add(element);
228 328
     notifyListeners();
229 329
 
@@ -244,6 +344,9 @@ class Editor extends ChangeNotifier {
244 344
       elementKey: GlobalKey()
245 345
     );
246 346
 
347
+    // Save History
348
+    addUndoEntry(CanvasHistoryModifyType.add, elementProperties);
349
+
247 350
     elementProperties.add(element);
248 351
     notifyListeners();
249 352
 
@@ -279,6 +382,9 @@ class Editor extends ChangeNotifier {
279 382
   void toggleLockElement() {
280 383
     if (selectedElm == null) return;
281 384
 
385
+    // ? Save History
386
+    addUndoEntry(CanvasHistoryModifyType.lock, elementProperties);
387
+
282 388
     selectedElm!.isLocked = !selectedElm!.isLocked;
283 389
     notifyListeners();
284 390
   }
@@ -417,7 +523,9 @@ class Editor extends ChangeNotifier {
417 523
     }
418 524
     
419 525
     if (![ElementType.text, ElementType.textbox].contains(element.type)) return;
420
-
526
+    
527
+    // ? Save history
528
+    addUndoEntry(CanvasHistoryModifyType.rotate, elementProperties);
421 529
 
422 530
     if (element.type == ElementType.text) _rotateText(element);
423 531
 
@@ -463,6 +571,9 @@ class Editor extends ChangeNotifier {
463 571
       return;
464 572
     }
465 573
 
574
+    // ? Save History
575
+    addUndoEntry(CanvasHistoryModifyType.resize, elementProperties);
576
+
466 577
     selectedElm!.fontScale = fontSize;
467 578
     notifyListeners();
468 579
   }
@@ -478,6 +589,9 @@ class Editor extends ChangeNotifier {
478 589
     }
479 590
     // check if value is allowed for resize
480 591
     if (CanvasStyle.fontSizeMap.containsKey(incrementTo)) {
592
+      // ? Save History
593
+      addUndoEntry(CanvasHistoryModifyType.resize, elementProperties);
594
+
481 595
       selectedElm!.fontScale = incrementTo;
482 596
       print('kepenjet increase');
483 597
     } else {
@@ -498,6 +612,9 @@ class Editor extends ChangeNotifier {
498 612
     }
499 613
     // check if value is allowed for resize
500 614
     if (CanvasStyle.fontSizeMap.containsKey(decrementTo)) {
615
+      // ? Save History
616
+      addUndoEntry(CanvasHistoryModifyType.resize, elementProperties);
617
+
501 618
       selectedElm!.fontScale = decrementTo;
502 619
     } else {
503 620
       print('cant decrement');
@@ -596,6 +713,9 @@ class Editor extends ChangeNotifier {
596 713
 
597 714
     if (!shouldDelete) return;
598 715
 
716
+    // ? Save History
717
+    addUndoEntry(CanvasHistoryModifyType.remove, elementProperties);
718
+
599 719
     elementProperties.removeWhere((e) => e.id == selectedElm!.id);
600 720
     unSelectElm();
601 721
 

+ 60 - 0
lib/test.dart

@@ -0,0 +1,60 @@
1
+// void main() {
2
+//   List<ElementProperty>elementProperties = [
3
+//     ElementProperty(top: 0, left: 0)
4
+//   ];
5
+
6
+//   print('Original:');
7
+//   elementProperties[0].printAll();
8
+//   print('\n\n\n');
9
+
10
+
11
+//   List<List<ElementProperty>> undoStack = [];
12
+
13
+//   // Add to undoStack
14
+//   undoStack.add(cloneState(elementProperties));
15
+
16
+//   // State changes
17
+//   elementProperties.first.top = 10;
18
+
19
+//   print('Original:');
20
+//   elementProperties[0].printAll();
21
+//   print('UndoStack:');
22
+//   undoStack.first.first.printAll();
23
+
24
+//   // Add to undoStack
25
+//   undoStack.add(cloneState(elementProperties));
26
+
27
+//   // State changes
28
+//   elementProperties.first.top = 20;
29
+
30
+//   print('Original2:');
31
+//   elementProperties[0].printAll();
32
+//   print('UndoStack2:');
33
+//   undoStack.first.first.printAll();
34
+//   undoStack.last.first.printAll();
35
+// }
36
+
37
+// List<ElementProperty> cloneState(List<ElementProperty> elementProperties) {
38
+//   List<ElementProperty> clonedElementProperties = elementProperties.map((elementProperty) {
39
+//     return ElementProperty(
40
+//       top: elementProperty.top,
41
+//       left: elementProperty.left,
42
+//     );
43
+//   }).toList();
44
+
45
+//   return clonedElementProperties;
46
+// }
47
+
48
+// class ElementProperty {
49
+//   double top;
50
+//   double left;
51
+
52
+//   ElementProperty({
53
+//     required this.top,
54
+//     required this.left,
55
+//   });
56
+
57
+//   void printAll() {
58
+//     print('top: $top, left: $left');
59
+//   }
60
+// }

+ 14 - 0
lib/text_debouncer_test.dart

@@ -0,0 +1,14 @@
1
+import 'package:easy_debounce/easy_throttle.dart';
2
+
3
+void main() {
4
+  EasyThrottle.throttle(
5
+    'my-debouncer', 
6
+    Duration(milliseconds: 1000), 
7
+    () => {
8
+      print('Executed !!!')
9
+    },
10
+    onAfter: () => {
11
+      print('After !!!')
12
+    }
13
+  );
14
+}

+ 63 - 7
lib/widgets/elements.dart

@@ -1,6 +1,11 @@
1
+import 'dart:developer';
2
+
1 3
 import 'package:defer_pointer/defer_pointer.dart';
4
+import 'package:easy_debounce/easy_debounce.dart';
5
+import 'package:easy_debounce/easy_throttle.dart';
2 6
 import 'package:flutter/gestures.dart';
3 7
 import 'package:flutter/material.dart';
8
+import 'package:flutter_canvas_editor/history.dart';
4 9
 import 'package:flutter_canvas_editor/providers/editor.dart';
5 10
 import 'package:intl/intl.dart';
6 11
 import 'package:provider/provider.dart';
@@ -76,6 +81,7 @@ class ElementProperty {
76 81
   int fontScale;
77 82
   int? qrScale; 
78 83
   bool isLocked;
84
+  String? lastSavedText;
79 85
 
80 86
   ElementProperty({
81 87
     required this.id,
@@ -90,7 +96,8 @@ class ElementProperty {
90 96
     this.fontScale = 3,
91 97
     this.qrScale,
92 98
     this.isLocked = false,
93
-    this.variableType
99
+    this.variableType,
100
+    this.lastSavedText
94 101
   });
95 102
 
96 103
 }
@@ -126,6 +133,11 @@ class _ElementWidgetState extends State<ElementWidget> {
126 133
 
127 134
   late TransformationController _transformController;
128 135
 
136
+  // // TODO: pindhkan ke state element property agar bisa dipake di widget Toolbar
137
+  // String? _lastSavedText;
138
+
139
+  FocusNode? _focus = FocusNode();
140
+
129 141
   @override
130 142
   void initState() {
131 143
     super.initState();
@@ -136,6 +148,7 @@ class _ElementWidgetState extends State<ElementWidget> {
136 148
 
137 149
     _setInitialScale();
138 150
     _listenTransformationChanges();
151
+    _addFocusNodeListener();
139 152
   }
140 153
 
141 154
 
@@ -158,12 +171,33 @@ class _ElementWidgetState extends State<ElementWidget> {
158 171
     _transformController.removeListener(_updateScale);
159 172
   }
160 173
 
174
+  void _addFocusNodeListener() {
175
+    if ([ElementType.text, ElementType.textbox].contains(widget.elementProperty.type)) {
176
+      _focus!.addListener(_onTextFieldFocusChange);
177
+    }
178
+  }
179
+
180
+  void _onTextFieldFocusChange() {
181
+    if (widget.elementProperty.lastSavedText != widget.elementProperty.valueController.text) {
182
+      print('executed');
183
+      Provider.of<Editor>(context, listen: false).addUndoEntry(CanvasHistoryModifyType.textEdit, Provider.of<Editor>(context, listen:  false).elementProperties);
184
+      widget.elementProperty.lastSavedText = widget.elementProperty.valueController.text;
185
+    }
186
+  }
187
+
188
+  void _removeFocusNodeListener() {
189
+    if ([ElementType.text, ElementType.textbox].contains(widget.elementProperty.type)) {
190
+      _focus!.removeListener(_onTextFieldFocusChange);
191
+    }
192
+  }
193
+
161 194
 
162 195
 
163 196
 
164 197
   @override
165 198
   void dispose() {
166 199
    _removeTransformationListener();
200
+   _removeFocusNodeListener();
167 201
     super.dispose();
168 202
   }
169 203
 
@@ -179,6 +213,8 @@ class _ElementWidgetState extends State<ElementWidget> {
179 213
       _height = element.elementKey.currentContext!.size!.height;
180 214
     });
181 215
 
216
+    List<ElementProperty> currentCanvasState = editorProvider.elementProperties;
217
+
182 218
     return Positioned(
183 219
       top: element.position.top,
184 220
       left: element.position.left,
@@ -192,6 +228,11 @@ class _ElementWidgetState extends State<ElementWidget> {
192 228
           print('Element Gesture Detector Tapped!');
193 229
           Provider.of<Editor>(context, listen: false).selectElmById(element.id);
194 230
         },
231
+        onPanStart: (details) {
232
+          log('Pan Start');
233
+          inspect(editorProvider.elementProperties);
234
+          editorProvider.addUndoEntry(CanvasHistoryModifyType.move, editorProvider.elementProperties);
235
+        },
195 236
         onPanUpdate: (details) {
196 237
           if (!Provider.of<Editor>(context, listen: false).isSelected(element.id)) {
197 238
             return;
@@ -288,7 +329,10 @@ class _ElementWidgetState extends State<ElementWidget> {
288 329
             }
289 330
 
290 331
 
291
-          Provider.of<Editor>(context, listen: false).updateElmPosition(Offset(adjustedElementPosition.left - element.position.left, adjustedElementPosition.top - element.position.top));
332
+          Provider.of<Editor>(context, listen: false).updateElmPosition(
333
+            Offset(adjustedElementPosition.left - element.position.left, adjustedElementPosition.top - element.position.top),
334
+            // Provider.of<Editor>(context, listen: false).elementProperties
335
+          );
292 336
         },
293 337
         child: Stack(
294 338
           clipBehavior: Clip.none,
@@ -489,6 +533,7 @@ class _ElementWidgetState extends State<ElementWidget> {
489 533
     if (Provider.of<Editor>(context, listen: true).isEditing && (Provider.of<Editor>(context, listen: true).selectedElmId == element.id)) {
490 534
       return IntrinsicWidth(
491 535
         child: TextField(
536
+          focusNode: _focus,
492 537
           controller: Provider.of<Editor>(context).selectedElm!.valueController,
493 538
           autofocus: true,
494 539
           keyboardType: TextInputType.multiline,
@@ -501,11 +546,22 @@ class _ElementWidgetState extends State<ElementWidget> {
501 546
             contentPadding: EdgeInsets.zero,
502 547
             border: InputBorder.none,
503 548
           ),
504
-          onChanged: (_) {
505
-            setState(() {
506
-              
507
-            });
508
-          }
549
+          onChanged: (newText) {
550
+
551
+            if (widget.elementProperty.lastSavedText != null && newText != widget.elementProperty.lastSavedText) {
552
+              EasyDebounce.debounce(
553
+                'text-debounce${element.id}', 
554
+                const Duration(milliseconds: 500), 
555
+                () {
556
+                  Provider.of<Editor>(context, listen: false).addUndoEntry(CanvasHistoryModifyType.textEdit, Provider.of<Editor>(context, listen: false).elementProperties);
557
+                  widget.elementProperty.lastSavedText = newText;
558
+                }
559
+              );
560
+
561
+            }
562
+
563
+            setState(() {});
564
+          },
509 565
         ),
510 566
       );
511 567
     }

+ 58 - 4
lib/widgets/toolbar.dart

@@ -1,4 +1,6 @@
1
+import 'package:easy_debounce/easy_debounce.dart';
1 2
 import 'package:flutter/material.dart';
3
+import 'package:flutter_canvas_editor/history.dart';
2 4
 import 'package:flutter_canvas_editor/providers/editor.dart';
3 5
 import 'package:flutter_canvas_editor/style/canvas_style.dart';
4 6
 import 'package:flutter_canvas_editor/widgets/elements.dart';
@@ -18,6 +20,10 @@ class _ToolbarWidgetState extends State<ToolbarWidget> {
18 20
 
19 21
   List<DropdownMenuItem<int>> qrSizeDropdownItems= [];
20 22
 
23
+  FocusNode? _focus = FocusNode();
24
+
25
+  // String? _lastSavedText;
26
+
21 27
 
22 28
   @override
23 29
   void initState() {
@@ -26,6 +32,10 @@ class _ToolbarWidgetState extends State<ToolbarWidget> {
26 32
 
27 33
     populateFontSizeDropdownItems();
28 34
     populateQrSizeDropdownItems();
35
+
36
+    WidgetsBinding.instance.addPostFrameCallback((_) {
37
+      _addFocusNodeListener();
38
+    });
29 39
   }
30 40
 
31 41
   //functions
@@ -53,6 +63,37 @@ class _ToolbarWidgetState extends State<ToolbarWidget> {
53 63
     });
54 64
   }
55 65
 
66
+  void _addFocusNodeListener() {
67
+    if (Provider.of<Editor>(context, listen: false).selectedElmKey != null && [ElementType.text, ElementType.textbox].contains(Provider.of<Editor>(context, listen: false).selectedElm!.type)) {
68
+      _focus!.addListener(_onTextFieldFocusChange);
69
+    }
70
+  }
71
+
72
+  void _onTextFieldFocusChange() {
73
+    print('toolbar onfocus change');
74
+
75
+    if (Provider.of<Editor>(context, listen: false).selectedElm?.lastSavedText != Provider.of<Editor>(context, listen: false).selectedElm?.valueController.text) {
76
+      Provider.of<Editor>(context, listen: false).addUndoEntry(CanvasHistoryModifyType.textEdit, Provider.of<Editor>(context, listen: false).elementProperties);
77
+      Provider.of<Editor>(context, listen: false).selectedElm?.lastSavedText = Provider.of<Editor>(context, listen: false).selectedElm?.valueController.text;
78
+    }
79
+  }
80
+
81
+  void _removeFocusNodeListener() {
82
+    if ([ElementType.text, ElementType.textbox].contains(Provider.of<Editor>(context).selectedElm!.type)) {
83
+      _focus!.removeListener(_onTextFieldFocusChange);
84
+    }
85
+  }
86
+
87
+
88
+  @override
89
+  void dispose() {
90
+    // TODO: implement dispose
91
+
92
+    _removeFocusNodeListener();
93
+    super.dispose();
94
+  }
95
+
96
+
56 97
   @override
57 98
   Widget build(BuildContext context) {
58 99
     final editorProvider = Provider.of<Editor>(context);
@@ -155,6 +196,7 @@ class _ToolbarWidgetState extends State<ToolbarWidget> {
155 196
     return [
156 197
       // ? Value Editor
157 198
       if([ElementType.text, ElementType.textbox].contains(editorProvider.selectedElm!.type)) TextField(
199
+        focusNode: _focus,
158 200
         readOnly: element!.isLocked, 
159 201
         controller: element!.valueController,
160 202
         onTap: editorProvider.enableEdit,
@@ -165,10 +207,22 @@ class _ToolbarWidgetState extends State<ToolbarWidget> {
165 207
 
166 208
           print('kepenjet');
167 209
         },
168
-        onChanged: (value) {
169
-          setState(() {
170
-            
171
-          });
210
+        onChanged: (newText) {
211
+          if (element.lastSavedText != null && newText != element.lastSavedText) {
212
+            EasyDebounce.debounce(
213
+              'text-debounce-toolbar${element.id}',
214
+              const Duration(milliseconds: 500),
215
+              () {
216
+                editorProvider.addUndoEntry(CanvasHistoryModifyType.textEdit, editorProvider.elementProperties);
217
+                element.lastSavedText = newText;
218
+              },
219
+              
220
+            );
221
+
222
+          }
223
+
224
+
225
+          setState(() {});
172 226
         },
173 227
       ),
174 228
       

+ 8 - 0
pubspec.lock

@@ -65,6 +65,14 @@ packages:
65 65
       url: "https://pub.dev"
66 66
     source: hosted
67 67
     version: "0.0.2"
68
+  easy_debounce:
69
+    dependency: "direct main"
70
+    description:
71
+      name: easy_debounce
72
+      sha256: f082609cfb8f37defb9e37fc28bc978c6712dedf08d4c5a26f820fa10165a236
73
+      url: "https://pub.dev"
74
+    source: hosted
75
+    version: "2.0.3"
68 76
   equatable:
69 77
     dependency: transitive
70 78
     description:

+ 1 - 0
pubspec.yaml

@@ -37,6 +37,7 @@ dependencies:
37 37
   uuid: ^4.5.1
38 38
   toastification: ^2.3.0
39 39
   intl: ^0.20.2
40
+  easy_debounce: ^2.0.3
40 41
 
41 42
 dev_dependencies:
42 43
   flutter_lints: ^3.0.0