Browse Source

feat: history-undo-redo (unstable)

Raihan Rizal 3 months ago
parent
commit
02d81a56cf

+ 1 - 1
lib/canvas_setup_page.dart

81
                       context: context, 
81
                       context: context, 
82
                       builder: (context) => AlertDialog(
82
                       builder: (context) => AlertDialog(
83
                         title: Text('Update Label Size'),
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
                         actions: [
85
                         actions: [
86
                           TextButton(
86
                           TextButton(
87
                             onPressed: () => Navigator.pop(context, true), 
87
                             onPressed: () => Navigator.pop(context, true), 

+ 13 - 0
lib/history.dart

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
 import 'package:defer_pointer/defer_pointer.dart';
3
 import 'package:defer_pointer/defer_pointer.dart';
4
 import 'package:flutter/material.dart';
4
 import 'package:flutter/material.dart';
5
 import 'package:flutter_canvas_editor/canvas_setup_page.dart';
5
 import 'package:flutter_canvas_editor/canvas_setup_page.dart';
6
+import 'package:flutter_canvas_editor/history.dart';
6
 import 'package:flutter_canvas_editor/providers/editor.dart';
7
 import 'package:flutter_canvas_editor/providers/editor.dart';
7
 import 'package:flutter_canvas_editor/snaptest_page.dart';
8
 import 'package:flutter_canvas_editor/snaptest_page.dart';
8
 import 'package:flutter_canvas_editor/test_page.dart';
9
 import 'package:flutter_canvas_editor/test_page.dart';
81
   // functions
82
   // functions
82
   void setInitialZoom() {
83
   void setInitialZoom() {
83
     Provider.of<Editor>(context, listen: false).setCanvasTransformationInitialZoom(context);
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
   void setTransformControllerListener() {
87
   void setTransformControllerListener() {
118
           title: Text('Template Editor'),
106
           title: Text('Template Editor'),
119
           actions: [
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
             // ! DEBUG BUTTON
122
             // ! DEBUG BUTTON
122
             IconButton(
123
             IconButton(
123
               onPressed: () {
124
               onPressed: () {

+ 122 - 2
lib/providers/editor.dart

3
 
3
 
4
 import 'package:defer_pointer/defer_pointer.dart';
4
 import 'package:defer_pointer/defer_pointer.dart';
5
 import 'package:flutter/material.dart';
5
 import 'package:flutter/material.dart';
6
+import 'package:flutter_canvas_editor/history.dart';
6
 import 'package:flutter_canvas_editor/style/canvas_style.dart';
7
 import 'package:flutter_canvas_editor/style/canvas_style.dart';
7
 import 'package:flutter_canvas_editor/widgets/elements.dart';
8
 import 'package:flutter_canvas_editor/widgets/elements.dart';
8
 import 'package:toastification/toastification.dart';
9
 import 'package:toastification/toastification.dart';
9
 import 'package:uuid/uuid.dart';
10
 import 'package:uuid/uuid.dart';
11
+import 'package:collection/collection.dart';
10
 
12
 
11
 import '../main.dart';
13
 import '../main.dart';
12
 
14
 
65
       qrScale: 3
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
   // ? udpate canvas
148
   // ? udpate canvas
71
   void updateCanvasProperty(BuildContext context, double width, double height) {
149
   void updateCanvasProperty(BuildContext context, double width, double height) {
72
     canvasProperty.height = height;
150
     canvasProperty.height = height;
73
     canvasProperty.width = width;
151
     canvasProperty.width = width;
152
+    
153
+    undoStack.clear();
154
+    redoStack.clear();
74
 
155
 
75
     _adjustOutOfBoundElement();
156
     _adjustOutOfBoundElement();
76
 
157
 
122
     );
203
     );
123
 
204
 
124
 
205
 
206
+    // Save History
207
+    addUndoEntry(CanvasHistoryModifyType.add, elementProperties);
208
+
209
+
125
     elementProperties.add(element);
210
     elementProperties.add(element);
126
     // _reservedIdUntil = elementProperties.length - 1;r
211
     // _reservedIdUntil = elementProperties.length - 1;r
127
     notifyListeners();
212
     notifyListeners();
142
       elementKey: GlobalKey()
227
       elementKey: GlobalKey()
143
     );
228
     );
144
 
229
 
230
+    // Save History
231
+    addUndoEntry(CanvasHistoryModifyType.add, elementProperties);
232
+
145
     elementProperties.add(element);
233
     elementProperties.add(element);
146
     notifyListeners();
234
     notifyListeners();
147
 
235
 
164
       elementKey: GlobalKey()
252
       elementKey: GlobalKey()
165
     );
253
     );
166
 
254
 
255
+    // Save History
256
+    addUndoEntry(CanvasHistoryModifyType.add, elementProperties);
257
+
167
     elementProperties.add(element);
258
     elementProperties.add(element);
168
     notifyListeners();
259
     notifyListeners();
169
 
260
 
184
       elementKey: GlobalKey()
275
       elementKey: GlobalKey()
185
     );
276
     );
186
 
277
 
278
+    // Save History
279
+    addUndoEntry(CanvasHistoryModifyType.add, elementProperties);
280
+
187
     elementProperties.add(element);
281
     elementProperties.add(element);
188
     notifyListeners();
282
     notifyListeners();
189
 
283
 
204
       elementKey: GlobalKey()
298
       elementKey: GlobalKey()
205
     );
299
     );
206
 
300
 
301
+    // Save History
302
+    addUndoEntry(CanvasHistoryModifyType.add, elementProperties);
303
+
207
     elementProperties.add(element);
304
     elementProperties.add(element);
208
     notifyListeners();
305
     notifyListeners();
209
 
306
 
224
       elementKey: GlobalKey()
321
       elementKey: GlobalKey()
225
     );
322
     );
226
 
323
 
324
+    // Save History
325
+    addUndoEntry(CanvasHistoryModifyType.add, elementProperties);
326
+
227
     elementProperties.add(element);
327
     elementProperties.add(element);
228
     notifyListeners();
328
     notifyListeners();
229
 
329
 
244
       elementKey: GlobalKey()
344
       elementKey: GlobalKey()
245
     );
345
     );
246
 
346
 
347
+    // Save History
348
+    addUndoEntry(CanvasHistoryModifyType.add, elementProperties);
349
+
247
     elementProperties.add(element);
350
     elementProperties.add(element);
248
     notifyListeners();
351
     notifyListeners();
249
 
352
 
279
   void toggleLockElement() {
382
   void toggleLockElement() {
280
     if (selectedElm == null) return;
383
     if (selectedElm == null) return;
281
 
384
 
385
+    // ? Save History
386
+    addUndoEntry(CanvasHistoryModifyType.lock, elementProperties);
387
+
282
     selectedElm!.isLocked = !selectedElm!.isLocked;
388
     selectedElm!.isLocked = !selectedElm!.isLocked;
283
     notifyListeners();
389
     notifyListeners();
284
   }
390
   }
417
     }
523
     }
418
     
524
     
419
     if (![ElementType.text, ElementType.textbox].contains(element.type)) return;
525
     if (![ElementType.text, ElementType.textbox].contains(element.type)) return;
420
-
526
+    
527
+    // ? Save history
528
+    addUndoEntry(CanvasHistoryModifyType.rotate, elementProperties);
421
 
529
 
422
     if (element.type == ElementType.text) _rotateText(element);
530
     if (element.type == ElementType.text) _rotateText(element);
423
 
531
 
463
       return;
571
       return;
464
     }
572
     }
465
 
573
 
574
+    // ? Save History
575
+    addUndoEntry(CanvasHistoryModifyType.resize, elementProperties);
576
+
466
     selectedElm!.fontScale = fontSize;
577
     selectedElm!.fontScale = fontSize;
467
     notifyListeners();
578
     notifyListeners();
468
   }
579
   }
478
     }
589
     }
479
     // check if value is allowed for resize
590
     // check if value is allowed for resize
480
     if (CanvasStyle.fontSizeMap.containsKey(incrementTo)) {
591
     if (CanvasStyle.fontSizeMap.containsKey(incrementTo)) {
592
+      // ? Save History
593
+      addUndoEntry(CanvasHistoryModifyType.resize, elementProperties);
594
+
481
       selectedElm!.fontScale = incrementTo;
595
       selectedElm!.fontScale = incrementTo;
482
       print('kepenjet increase');
596
       print('kepenjet increase');
483
     } else {
597
     } else {
498
     }
612
     }
499
     // check if value is allowed for resize
613
     // check if value is allowed for resize
500
     if (CanvasStyle.fontSizeMap.containsKey(decrementTo)) {
614
     if (CanvasStyle.fontSizeMap.containsKey(decrementTo)) {
615
+      // ? Save History
616
+      addUndoEntry(CanvasHistoryModifyType.resize, elementProperties);
617
+
501
       selectedElm!.fontScale = decrementTo;
618
       selectedElm!.fontScale = decrementTo;
502
     } else {
619
     } else {
503
       print('cant decrement');
620
       print('cant decrement');
596
 
713
 
597
     if (!shouldDelete) return;
714
     if (!shouldDelete) return;
598
 
715
 
716
+    // ? Save History
717
+    addUndoEntry(CanvasHistoryModifyType.remove, elementProperties);
718
+
599
     elementProperties.removeWhere((e) => e.id == selectedElm!.id);
719
     elementProperties.removeWhere((e) => e.id == selectedElm!.id);
600
     unSelectElm();
720
     unSelectElm();
601
 
721
 

+ 60 - 0
lib/test.dart

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

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
+import 'dart:developer';
2
+
1
 import 'package:defer_pointer/defer_pointer.dart';
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
 import 'package:flutter/gestures.dart';
6
 import 'package:flutter/gestures.dart';
3
 import 'package:flutter/material.dart';
7
 import 'package:flutter/material.dart';
8
+import 'package:flutter_canvas_editor/history.dart';
4
 import 'package:flutter_canvas_editor/providers/editor.dart';
9
 import 'package:flutter_canvas_editor/providers/editor.dart';
5
 import 'package:intl/intl.dart';
10
 import 'package:intl/intl.dart';
6
 import 'package:provider/provider.dart';
11
 import 'package:provider/provider.dart';
76
   int fontScale;
81
   int fontScale;
77
   int? qrScale; 
82
   int? qrScale; 
78
   bool isLocked;
83
   bool isLocked;
84
+  String? lastSavedText;
79
 
85
 
80
   ElementProperty({
86
   ElementProperty({
81
     required this.id,
87
     required this.id,
90
     this.fontScale = 3,
96
     this.fontScale = 3,
91
     this.qrScale,
97
     this.qrScale,
92
     this.isLocked = false,
98
     this.isLocked = false,
93
-    this.variableType
99
+    this.variableType,
100
+    this.lastSavedText
94
   });
101
   });
95
 
102
 
96
 }
103
 }
126
 
133
 
127
   late TransformationController _transformController;
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
   @override
141
   @override
130
   void initState() {
142
   void initState() {
131
     super.initState();
143
     super.initState();
136
 
148
 
137
     _setInitialScale();
149
     _setInitialScale();
138
     _listenTransformationChanges();
150
     _listenTransformationChanges();
151
+    _addFocusNodeListener();
139
   }
152
   }
140
 
153
 
141
 
154
 
158
     _transformController.removeListener(_updateScale);
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
   @override
197
   @override
165
   void dispose() {
198
   void dispose() {
166
    _removeTransformationListener();
199
    _removeTransformationListener();
200
+   _removeFocusNodeListener();
167
     super.dispose();
201
     super.dispose();
168
   }
202
   }
169
 
203
 
179
       _height = element.elementKey.currentContext!.size!.height;
213
       _height = element.elementKey.currentContext!.size!.height;
180
     });
214
     });
181
 
215
 
216
+    List<ElementProperty> currentCanvasState = editorProvider.elementProperties;
217
+
182
     return Positioned(
218
     return Positioned(
183
       top: element.position.top,
219
       top: element.position.top,
184
       left: element.position.left,
220
       left: element.position.left,
192
           print('Element Gesture Detector Tapped!');
228
           print('Element Gesture Detector Tapped!');
193
           Provider.of<Editor>(context, listen: false).selectElmById(element.id);
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
         onPanUpdate: (details) {
236
         onPanUpdate: (details) {
196
           if (!Provider.of<Editor>(context, listen: false).isSelected(element.id)) {
237
           if (!Provider.of<Editor>(context, listen: false).isSelected(element.id)) {
197
             return;
238
             return;
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
         child: Stack(
337
         child: Stack(
294
           clipBehavior: Clip.none,
338
           clipBehavior: Clip.none,
489
     if (Provider.of<Editor>(context, listen: true).isEditing && (Provider.of<Editor>(context, listen: true).selectedElmId == element.id)) {
533
     if (Provider.of<Editor>(context, listen: true).isEditing && (Provider.of<Editor>(context, listen: true).selectedElmId == element.id)) {
490
       return IntrinsicWidth(
534
       return IntrinsicWidth(
491
         child: TextField(
535
         child: TextField(
536
+          focusNode: _focus,
492
           controller: Provider.of<Editor>(context).selectedElm!.valueController,
537
           controller: Provider.of<Editor>(context).selectedElm!.valueController,
493
           autofocus: true,
538
           autofocus: true,
494
           keyboardType: TextInputType.multiline,
539
           keyboardType: TextInputType.multiline,
501
             contentPadding: EdgeInsets.zero,
546
             contentPadding: EdgeInsets.zero,
502
             border: InputBorder.none,
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
+import 'package:easy_debounce/easy_debounce.dart';
1
 import 'package:flutter/material.dart';
2
 import 'package:flutter/material.dart';
3
+import 'package:flutter_canvas_editor/history.dart';
2
 import 'package:flutter_canvas_editor/providers/editor.dart';
4
 import 'package:flutter_canvas_editor/providers/editor.dart';
3
 import 'package:flutter_canvas_editor/style/canvas_style.dart';
5
 import 'package:flutter_canvas_editor/style/canvas_style.dart';
4
 import 'package:flutter_canvas_editor/widgets/elements.dart';
6
 import 'package:flutter_canvas_editor/widgets/elements.dart';
18
 
20
 
19
   List<DropdownMenuItem<int>> qrSizeDropdownItems= [];
21
   List<DropdownMenuItem<int>> qrSizeDropdownItems= [];
20
 
22
 
23
+  FocusNode? _focus = FocusNode();
24
+
25
+  // String? _lastSavedText;
26
+
21
 
27
 
22
   @override
28
   @override
23
   void initState() {
29
   void initState() {
26
 
32
 
27
     populateFontSizeDropdownItems();
33
     populateFontSizeDropdownItems();
28
     populateQrSizeDropdownItems();
34
     populateQrSizeDropdownItems();
35
+
36
+    WidgetsBinding.instance.addPostFrameCallback((_) {
37
+      _addFocusNodeListener();
38
+    });
29
   }
39
   }
30
 
40
 
31
   //functions
41
   //functions
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
   @override
97
   @override
57
   Widget build(BuildContext context) {
98
   Widget build(BuildContext context) {
58
     final editorProvider = Provider.of<Editor>(context);
99
     final editorProvider = Provider.of<Editor>(context);
155
     return [
196
     return [
156
       // ? Value Editor
197
       // ? Value Editor
157
       if([ElementType.text, ElementType.textbox].contains(editorProvider.selectedElm!.type)) TextField(
198
       if([ElementType.text, ElementType.textbox].contains(editorProvider.selectedElm!.type)) TextField(
199
+        focusNode: _focus,
158
         readOnly: element!.isLocked, 
200
         readOnly: element!.isLocked, 
159
         controller: element!.valueController,
201
         controller: element!.valueController,
160
         onTap: editorProvider.enableEdit,
202
         onTap: editorProvider.enableEdit,
165
 
207
 
166
           print('kepenjet');
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
       url: "https://pub.dev"
65
       url: "https://pub.dev"
66
     source: hosted
66
     source: hosted
67
     version: "0.0.2"
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
   equatable:
76
   equatable:
69
     dependency: transitive
77
     dependency: transitive
70
     description:
78
     description:

+ 1 - 0
pubspec.yaml

37
   uuid: ^4.5.1
37
   uuid: ^4.5.1
38
   toastification: ^2.3.0
38
   toastification: ^2.3.0
39
   intl: ^0.20.2
39
   intl: ^0.20.2
40
+  easy_debounce: ^2.0.3
40
 
41
 
41
 dev_dependencies:
42
 dev_dependencies:
42
   flutter_lints: ^3.0.0
43
   flutter_lints: ^3.0.0