2 Incheckningar 678474eed8 ... 56f0bd690d

Upphovsman SHA1 Meddelande Datum
  Raihan Rizal 56f0bd690d feat: history-undo-redo (refactored-stable) 3 månader sedan
  Raihan Rizal 02d81a56cf feat: history-undo-redo (unstable) 4 månader sedan
11 ändrade filer med 1091 tillägg och 473 borttagningar
  1. 32 3
      lib/canvas_setup_page.dart
  2. 13 0
      lib/history.dart
  3. 15 14
      lib/main.dart
  4. 389 104
      lib/providers/editor.dart
  5. 60 0
      lib/test.dart
  6. 14 0
      lib/text_debouncer_test.dart
  7. 32 0
      lib/utils/debouncer.dart
  8. 377 286
      lib/widgets/elements.dart
  9. 150 66
      lib/widgets/toolbar.dart
  10. 8 0
      pubspec.lock
  11. 1 0
      pubspec.yaml

+ 32 - 3
lib/canvas_setup_page.dart

51
 
51
 
52
               TextFormField(
52
               TextFormField(
53
                 controller: _widthController,
53
                 controller: _widthController,
54
-                decoration: InputDecoration(labelText: 'Width'),
54
+                decoration: InputDecoration(labelText: 'Width (mm)'),
55
                 keyboardType: TextInputType.number,
55
                 keyboardType: TextInputType.number,
56
                 validator: (value) {
56
                 validator: (value) {
57
                   if (value == null || value.isEmpty) {
57
                   if (value == null || value.isEmpty) {
58
                     return 'Please enter width';
58
                     return 'Please enter width';
59
                   }
59
                   }
60
+
61
+                  double? parsedValue = double.tryParse(value);
62
+
63
+                  if (parsedValue is! double) {
64
+                    return 'Not a valid input';
65
+                  }
66
+
67
+                  if (parsedValue < 30) {
68
+                    return 'Width should be >= 30';
69
+                  }
70
+
71
+                  if (parsedValue > 1000) {
72
+                    return 'Width should be > 1000';
73
+                  }
74
+
60
                   return null;
75
                   return null;
61
                 },
76
                 },
62
               ),
77
               ),
63
               TextFormField(
78
               TextFormField(
64
                 controller: _heightController,
79
                 controller: _heightController,
65
-                decoration: InputDecoration(labelText: 'Height'),
80
+                decoration: InputDecoration(labelText: 'Height (mm)'),
66
                 keyboardType: TextInputType.number,
81
                 keyboardType: TextInputType.number,
67
                 validator: (value) {
82
                 validator: (value) {
68
                   if (value == null || value.isEmpty) {
83
                   if (value == null || value.isEmpty) {
69
                     return 'Please enter height';
84
                     return 'Please enter height';
70
                   }
85
                   }
86
+
87
+                  double? parsedValue = double.tryParse(value);
88
+
89
+                  if (parsedValue is! double) {
90
+                    return 'Not a valid input';
91
+                  }
92
+
93
+                  if (parsedValue < 15) {
94
+                    return 'Width should be >= 15';
95
+                  }
96
+
97
+                  if (parsedValue > 1000) {
98
+                    return 'Width should be >= 1000';
99
+                  }
71
                   return null;
100
                   return null;
72
                 },
101
                 },
73
               ),
102
               ),
81
                       context: context, 
110
                       context: context, 
82
                       builder: (context) => AlertDialog(
111
                       builder: (context) => AlertDialog(
83
                         title: Text('Update Label Size'),
112
                         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 ?'),
113
+                        content: Text('updating label size will remove all history you have changed, Are you sure want to update label size ?'),
85
                         actions: [
114
                         actions: [
86
                           TextButton(
115
                           TextButton(
87
                             onPressed: () => Navigator.pop(context, true), 
116
                             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<ElementState> elementPropeties;
9
+
10
+  CanvasHistory(this.type, this.elementPropeties);
11
+
12
+  List<ElementState> get getState => elementPropeties;
13
+}

+ 15 - 14
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).stateStack.length <= 1 ? 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: () {
164
                             width: Provider.of<Editor>(context).canvasProperty.width,
165
                             width: Provider.of<Editor>(context).canvasProperty.width,
165
                             child: Stack(
166
                             child: Stack(
166
                               children: [
167
                               children: [
167
-                                for (ElementProperty elementProperty in Provider.of<Editor>(context).elementProperties) ... [
168
+                                for (ElementState elementProperty in Provider.of<Editor>(context).currentElementsState) ... [
168
                                   ElementWidget(
169
                                   ElementWidget(
169
                                     elementProperty: elementProperty,
170
                                     elementProperty: elementProperty,
170
                                     canvasProperty: Provider.of<Editor>(context).canvasProperty,
171
                                     canvasProperty: Provider.of<Editor>(context).canvasProperty,

+ 389 - 104
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
 
21
 
23
 
22
   final textboxResizerDeferredPointerHandlerLink = DeferredPointerHandlerLink();
24
   final textboxResizerDeferredPointerHandlerLink = DeferredPointerHandlerLink();
23
 
25
 
26
+  String? valueOnStartEditing;
24
 
27
 
25
 
28
 
26
   // ? Canvas State
29
   // ? Canvas State
53
 
56
 
54
 
57
 
55
 
58
 
56
-  List<ElementProperty> elementProperties = [
57
-    ElementProperty(
58
-      id: uuid.v4(), 
59
-      valueController: TextEditingController(text: '{{QRCODE}}'), 
60
-      type: ElementType.qr, 
61
-      position: ElementPosition(top: 0, left: 0), 
62
-      width: 80, 
63
-      quarterTurns: 0, 
64
-      elementKey: GlobalKey(),
65
-      qrScale: 3
66
-    )
59
+  // List<ElementProperty> elementProperties = [
60
+  //   ElementProperty(
61
+  //     id: uuid.v4(), 
62
+  //     valueController: TextEditingController(text: '{{QRCODE}}'), 
63
+  //     type: ElementType.qr, 
64
+  //     position: ElementPosition(top: 0, left: 0), 
65
+  //     width: 80, 
66
+  //     quarterTurns: 0, 
67
+  //     elementKey: GlobalKey(),
68
+  //     qrScale: 3
69
+  //   )
70
+  // ];
71
+
72
+  // This list store all changes history, and currentCanvasState (stackState.last)
73
+  List<List<ElementState>> stateStack = [
74
+    // ? default state
75
+    [
76
+      ElementState(
77
+        id: uuid.v4(), 
78
+        valueController: TextEditingController(text: '{{QRCODE}}'), 
79
+        type: ElementType.qr, 
80
+        position: ElementPosition(top: 0, left: 0), 
81
+        width: 80, 
82
+        quarterTurns: 0, 
83
+        elementKey: GlobalKey(),
84
+        qrScale: 3
85
+      )
86
+    ]
67
   ];
87
   ];
68
-  // int _reservedIdUntil = 0;
88
+
89
+  // current canvas state
90
+  List<ElementState> get currentElementsState => stateStack.last;
91
+
92
+
93
+
94
+
95
+  // ? History stack
96
+  // List<CanvasHistory> undoStack = [];
97
+  List<List<ElementState>> redoStack = [];
98
+
99
+
100
+  void setNewElementsState(List<ElementState> newElementsState) {
101
+    _setNewStateTextEdit(newElementsState);
102
+
103
+    stateStack.add(newElementsState);
104
+
105
+    redoStack.clear();
106
+
107
+    notifyListeners();
108
+  }
109
+
110
+  void undo() {
111
+    addRedoEntry(stateStack.last);
112
+
113
+    // apply to current state
114
+    // if (undoStack.last.type == CanvasHistoryModifyType.textEdit && undoStack.last.getState != elementProperties) {
115
+    //   undoStack.removeLast();
116
+    // }
117
+
118
+    // elementProperties = undoStack.last.getState;
119
+
120
+    stateStack.removeLast();
121
+
122
+    // unselect element
123
+    if (currentElementsState.firstWhereOrNull((e) => e.id == selectedElmId) == null) {
124
+      unSelectElm();
125
+    }
126
+
127
+    notifyListeners();
128
+  }
129
+
130
+
131
+  void addRedoEntry(List<ElementState> elementProperties) {
132
+    redoStack.add(_cloneElementsState(elementProperties));
133
+
134
+    notifyListeners();
135
+  }
136
+
137
+  void redo() {
138
+    // undoStack.add(CanvasHistory(CanvasHistoryModifyType.redo, _cloneCurrentState(elementProperties)));
139
+    stateStack.add(_cloneElementsState(redoStack.last));
140
+     
141
+    // apply to current state
142
+    // elementProperties = redoStack.last;
143
+
144
+    redoStack.removeLast();
145
+
146
+    notifyListeners();
147
+  }
148
+
149
+  List<ElementState> _cloneElementsState(List<ElementState> elementProperties) {
150
+    List<ElementState> clonedElementProperties = elementProperties.map((elementProperty) {
151
+      return ElementState(
152
+        id: elementProperty.id,
153
+        valueController: TextEditingController(text: elementProperty.valueController.text),
154
+        type: elementProperty.type,
155
+        position: ElementPosition(top: elementProperty.position.top, left: elementProperty.position.left),
156
+        width: elementProperty.width,
157
+        quarterTurns: elementProperty.quarterTurns,
158
+        elementKey: elementProperty.elementKey,
159
+        qrScale: elementProperty.qrScale,
160
+        fontScale: elementProperty.fontScale,
161
+        variableType: elementProperty.variableType,
162
+        isLocked: elementProperty.isLocked
163
+      );
164
+    }).toList();
165
+
166
+    return clonedElementProperties;
167
+  }
168
+
169
+
170
+
69
 
171
 
70
   // ? udpate canvas
172
   // ? udpate canvas
71
   void updateCanvasProperty(BuildContext context, double width, double height) {
173
   void updateCanvasProperty(BuildContext context, double width, double height) {
72
     canvasProperty.height = height;
174
     canvasProperty.height = height;
73
     canvasProperty.width = width;
175
     canvasProperty.width = width;
176
+    
177
+    // undoStack.clear();
178
+    stateStack = [currentElementsState];
179
+    redoStack.clear();
74
 
180
 
75
     _adjustOutOfBoundElement();
181
     _adjustOutOfBoundElement();
76
 
182
 
80
   }
186
   }
81
 
187
 
82
   void _adjustOutOfBoundElement() {
188
   void _adjustOutOfBoundElement() {
83
-    for (var element in elementProperties) {
189
+    for (var element in currentElementsState) {
84
       bool isOutOfBoundFromTop = (element.position.top + 10) > canvasProperty.height;
190
       bool isOutOfBoundFromTop = (element.position.top + 10) > canvasProperty.height;
85
       bool isOutOfBoundFromLeft = (element.position.left + 10) > canvasProperty.width;
191
       bool isOutOfBoundFromLeft = (element.position.left + 10) > canvasProperty.width;
86
 
192
 
101
 
207
 
102
   }
208
   }
103
 
209
 
104
-  void populateElement(List<ElementProperty> elementProperties) {
105
-    this.elementProperties.addAll(elementProperties);
106
-    // _reservedIdUntil = this.elementProperties.length - 1;
107
-    notifyListeners();
108
-  }
109
 
210
 
211
+
212
+
213
+
214
+
215
+  // ? Primitive Element
110
   void addTextElement(){
216
   void addTextElement(){
111
     String id = uuid.v4();
217
     String id = uuid.v4();
112
 
218
 
113
-    ElementProperty element = ElementProperty(
219
+    ElementState newElement = ElementState(
114
       id: id, 
220
       id: id, 
115
       valueController: TextEditingController(text: 'Double tap to edit text'), 
221
       valueController: TextEditingController(text: 'Double tap to edit text'), 
116
       type: ElementType.text, 
222
       type: ElementType.text, 
122
     );
228
     );
123
 
229
 
124
 
230
 
125
-    elementProperties.add(element);
126
-    // _reservedIdUntil = elementProperties.length - 1;r
127
-    notifyListeners();
231
+    // Set State
232
+    // List<ElementState> newElementsState = [..._cloneElementsState(currentElementsState), newElement];
233
+    // setNewElementsState(newElementsState);
234
+    _setAddNewElementState(newElement);
128
 
235
 
236
+
237
+    notifyListeners();
129
     selectElmById(id);
238
     selectElmById(id);
130
   } 
239
   } 
131
 
240
 
132
   void addTextboxElement() {
241
   void addTextboxElement() {
133
     String id = uuid.v4();
242
     String id = uuid.v4();
134
 
243
 
135
-    ElementProperty element = ElementProperty(
244
+    ElementState newElement = ElementState(
136
       id: id, 
245
       id: id, 
137
       valueController: TextEditingController(text: 'Double tap to edit text'), 
246
       valueController: TextEditingController(text: 'Double tap to edit text'), 
138
       type: ElementType.textbox, 
247
       type: ElementType.textbox, 
142
       elementKey: GlobalKey()
251
       elementKey: GlobalKey()
143
     );
252
     );
144
 
253
 
145
-    elementProperties.add(element);
146
-    notifyListeners();
147
 
254
 
255
+    // Set State
256
+    _setAddNewElementState(newElement);
257
+
258
+    notifyListeners();
148
     selectElmById(id);
259
     selectElmById(id);
149
   }
260
   }
150
 
261
 
151
-  // ? Variable Element
152
 
262
 
263
+
264
+
265
+
266
+  // ? Variable Element
153
   void addProductNameElement() {
267
   void addProductNameElement() {
154
     String id = uuid.v4();
268
     String id = uuid.v4();
155
 
269
 
156
-    ElementProperty element = ElementProperty(
270
+    ElementState newElement = ElementState(
157
       id: id, 
271
       id: id, 
158
       valueController: TextEditingController(text: '{{PRODUCTNAME}}'), 
272
       valueController: TextEditingController(text: '{{PRODUCTNAME}}'), 
159
       type: ElementType.textbox,
273
       type: ElementType.textbox,
164
       elementKey: GlobalKey()
278
       elementKey: GlobalKey()
165
     );
279
     );
166
 
280
 
167
-    elementProperties.add(element);
168
-    notifyListeners();
281
+    // Set State
282
+    _setAddNewElementState(newElement);
169
 
283
 
284
+    notifyListeners();
170
     selectElmById(id);
285
     selectElmById(id);
171
   }
286
   }
172
 
287
 
173
   void addVariantNameElement() {
288
   void addVariantNameElement() {
174
     String id = uuid.v4();
289
     String id = uuid.v4();
175
 
290
 
176
-    ElementProperty element = ElementProperty(
291
+    ElementState newElement = ElementState(
177
       id: id, 
292
       id: id, 
178
       valueController: TextEditingController(text: '{{VARIANTNAME}}'), 
293
       valueController: TextEditingController(text: '{{VARIANTNAME}}'), 
179
       type: ElementType.textbox,
294
       type: ElementType.textbox,
184
       elementKey: GlobalKey()
299
       elementKey: GlobalKey()
185
     );
300
     );
186
 
301
 
187
-    elementProperties.add(element);
188
-    notifyListeners();
302
+    // Set State
303
+    _setAddNewElementState(newElement);
189
 
304
 
305
+    notifyListeners();
190
     selectElmById(id);
306
     selectElmById(id);
191
   }
307
   }
192
 
308
 
193
   void addProductionCodeElement() {
309
   void addProductionCodeElement() {
194
     String id = uuid.v4();
310
     String id = uuid.v4();
195
 
311
 
196
-    ElementProperty element = ElementProperty(
312
+    ElementState newElement = ElementState(
197
       id: id, 
313
       id: id, 
198
       valueController: TextEditingController(text: '{{PRODUCTIONCODE}}'), 
314
       valueController: TextEditingController(text: '{{PRODUCTIONCODE}}'), 
199
       type: ElementType.textbox,
315
       type: ElementType.textbox,
204
       elementKey: GlobalKey()
320
       elementKey: GlobalKey()
205
     );
321
     );
206
 
322
 
207
-    elementProperties.add(element);
208
-    notifyListeners();
323
+    // Set State
324
+    _setAddNewElementState(newElement);
209
 
325
 
326
+    notifyListeners();
210
     selectElmById(id);
327
     selectElmById(id);
211
   }
328
   }
212
 
329
 
213
   void addProductionDateElement() {
330
   void addProductionDateElement() {
214
     String id = uuid.v4();
331
     String id = uuid.v4();
215
 
332
 
216
-    ElementProperty element = ElementProperty(
333
+    ElementState newElement = ElementState(
217
       id: id, 
334
       id: id, 
218
       valueController: TextEditingController(text: '{{PRODUCTIONDATE}}'), 
335
       valueController: TextEditingController(text: '{{PRODUCTIONDATE}}'), 
219
       type: ElementType.text,
336
       type: ElementType.text,
224
       elementKey: GlobalKey()
341
       elementKey: GlobalKey()
225
     );
342
     );
226
 
343
 
227
-    elementProperties.add(element);
228
-    notifyListeners();
344
+    // Save History
345
+    _setAddNewElementState(newElement);
229
 
346
 
347
+    notifyListeners();
230
     selectElmById(id);
348
     selectElmById(id);
231
   }
349
   }
232
 
350
 
233
   void addSerialNumberElement() {
351
   void addSerialNumberElement() {
234
     String id = uuid.v4();
352
     String id = uuid.v4();
235
 
353
 
236
-    ElementProperty element = ElementProperty(
354
+    ElementState newElement = ElementState(
237
       id: id, 
355
       id: id, 
238
       valueController: TextEditingController(text: '{{SERIALNUMBER}}'), 
356
       valueController: TextEditingController(text: '{{SERIALNUMBER}}'), 
239
       type: ElementType.text,
357
       type: ElementType.text,
244
       elementKey: GlobalKey()
362
       elementKey: GlobalKey()
245
     );
363
     );
246
 
364
 
247
-    elementProperties.add(element);
248
-    notifyListeners();
365
+    // Set State
366
+    _setAddNewElementState(newElement);
249
 
367
 
368
+    notifyListeners();
250
     selectElmById(id);
369
     selectElmById(id);
251
   }
370
   }
252
-  
371
+
253
 
372
 
254
 
373
 
255
 
374
 
256
 
375
 
257
   void updateElmPosition(Offset offset) {
376
   void updateElmPosition(Offset offset) {
258
-    ElementProperty? element = selectedElm;
377
+    ElementState? element = selectedElm;
259
 
378
 
260
     if (element == null) return;
379
     if (element == null) return;
261
 
380
 
266
     notifyListeners();
385
     notifyListeners();
267
   }
386
   }
268
 
387
 
388
+  // Reset position after drag end
389
+  void resetElmPosition(ElementPosition? elementPosition) {
390
+    ElementState? element = selectedElm;
391
+
392
+    if (element == null) return;
393
+
394
+    if (elementPosition == null) return;
395
+
396
+    element.position = elementPosition;
397
+  }
398
+
399
+  void moveElement(Offset offset) {
400
+    log('[MOVING ELEMENT]');
401
+    ElementState? element = selectedElm;
402
+
403
+    if (element == null) return;
404
+
405
+    List<ElementState> newElementsState = _cloneElementsState(currentElementsState);
406
+    var newElement = newElementsState.firstWhere((e) => e.id == selectedElmId);
407
+
408
+    newElement.position.top += offset.dy.round();
409
+    newElement.position.left += offset.dx.round();
410
+
411
+    setNewElementsState(newElementsState);
412
+  }
413
+
414
+  ElementPosition getClonedElementPosition(ElementState element) {
415
+    return ElementPosition(
416
+      top: element.position.top, 
417
+      left: element.position.left
418
+    );
419
+  }
420
+
421
+
269
   void updateElmWitdh(double width) {
422
   void updateElmWitdh(double width) {
270
-    ElementProperty? element = selectedElm;
423
+    ElementState? element = selectedElm;
271
 
424
 
272
     if (element == null) return;
425
     if (element == null) return;
273
 
426
 
276
     notifyListeners();
429
     notifyListeners();
277
   }
430
   }
278
 
431
 
432
+  void commitTextboxResize(double width, double textboxDragStartWidth) {
433
+    if (selectedElm == null) return;
434
+
435
+    List<ElementState> newElementsState = _cloneElementsState(currentElementsState);
436
+    var newElement = newElementsState.firstWhere((e) => e.id == selectedElmId);
437
+
438
+    newElement.width = width;
439
+
440
+    // reset previous element
441
+    _resetPreviousTextboxWidth(selectedElm!, textboxDragStartWidth);
442
+
443
+    setNewElementsState(newElementsState);
444
+  }
445
+
446
+  void _resetPreviousTextboxWidth(ElementState previousElement, double textboxDragStartWidth) {
447
+    if (selectedElm == null) return;
448
+
449
+    previousElement.width = textboxDragStartWidth;
450
+  }
451
+
279
   void toggleLockElement() {
452
   void toggleLockElement() {
280
     if (selectedElm == null) return;
453
     if (selectedElm == null) return;
281
 
454
 
282
-    selectedElm!.isLocked = !selectedElm!.isLocked;
455
+    List<ElementState> newElementsState = _cloneElementsState(currentElementsState);
456
+    var selectedNewElement = newElementsState.firstWhere((e) => e.id == selectedElm!.id);
457
+    
458
+    selectedNewElement.isLocked = !selectedNewElement.isLocked;
459
+
460
+    setNewElementsState(newElementsState);
461
+
462
+    // selectedElm!.isLocked = !selectedElm!.isLocked;
283
     notifyListeners();
463
     notifyListeners();
284
   }
464
   }
285
 
465
 
466
+  bool shouldIgnoreTouch(String elementId) {
467
+    if (elementId == selectedElmId) return false;
468
+
469
+    if (selectedElmId == null) return false;
470
+
471
+    return true;
472
+  }
286
 
473
 
287
   void selectElmById(String id) {
474
   void selectElmById(String id) {
288
     selectedElmId = id;
475
     selectedElmId = id;
476
+    valueOnStartEditing = currentElementsState.firstWhere((e) => e.id == selectedElmId).valueController.text;
289
     notifyListeners();
477
     notifyListeners();
290
   }
478
   }
291
 
479
 
309
   void unSelectElm() {
497
   void unSelectElm() {
310
     selectedElmId = null;
498
     selectedElmId = null;
311
     isEditing = false;
499
     isEditing = false;
500
+    valueOnStartEditing = null;
312
     notifyListeners();
501
     notifyListeners();
313
   }
502
   }
314
 
503
 
318
 
507
 
319
   // ? Getters
508
   // ? Getters
320
   String get selectedElmType {
509
   String get selectedElmType {
321
-    if (elementProperties.isNotEmpty && selectedElmId != null) {
322
-      final selectedElm = elementProperties.firstWhere((element) => element.id == selectedElmId);
510
+    if (currentElementsState.isNotEmpty && selectedElmId != null) {
511
+      final selectedElm = currentElementsState.firstWhere((element) => element.id == selectedElmId);
323
       return elementGetter(selectedElm.type);
512
       return elementGetter(selectedElm.type);
324
     }
513
     }
325
 
514
 
326
     return '';
515
     return '';
327
   }
516
   }
328
 
517
 
329
-  ElementProperty? get selectedElm {
330
-    if (elementProperties.isNotEmpty && selectedElmId != null) {
331
-      return elementProperties.firstWhere((element) => element.id == selectedElmId);
518
+  ElementState? get selectedElm {
519
+    if (currentElementsState.isNotEmpty && selectedElmId != null) {
520
+      return currentElementsState.firstWhereOrNull((element) => element.id == selectedElmId);
332
     }
521
     }
333
 
522
 
334
     return null;
523
     return null;
335
   }
524
   }
336
 
525
 
337
   GlobalKey? get selectedElmKey {
526
   GlobalKey? get selectedElmKey {
338
-    if (elementProperties.isNotEmpty && selectedElmId != null) {
339
-      return elementProperties.firstWhere((element) => element.id == selectedElmId).elementKey;
527
+    if (currentElementsState.isNotEmpty && selectedElmId != null) {
528
+      return currentElementsState.firstWhere((element) => element.id == selectedElmId).elementKey;
340
     }
529
     }
341
 
530
 
342
     return null;
531
     return null;
407
 
596
 
408
   /// Can only rotate [ElementType.text, ElementType.textBox]
597
   /// Can only rotate [ElementType.text, ElementType.textBox]
409
   void rotate() {
598
   void rotate() {
410
-    ElementProperty? element = selectedElm;
411
-
412
-    if (element == null) return;
599
+    if (selectedElm == null) return;
413
 
600
 
414
-    if (element.isLocked) {
601
+    if (selectedElm!.isLocked) {
415
       _showLockedToast('Cant rotate locked element');
602
       _showLockedToast('Cant rotate locked element');
416
       return;
603
       return;
417
     }
604
     }
418
     
605
     
419
-    if (![ElementType.text, ElementType.textbox].contains(element.type)) return;
606
+    if (![ElementType.text, ElementType.textbox].contains(selectedElm!.type)) return;
607
+    
608
+    List<ElementState> newElementsState = _cloneElementsState(currentElementsState);
609
+    var newElement = newElementsState.firstWhere((e) => e.id == selectedElmId);
610
+
420
 
611
 
612
+    if (newElement.type == ElementType.text) _rotateText(newElement);
421
 
613
 
422
-    if (element.type == ElementType.text) _rotateText(element);
614
+    if (newElement.type == ElementType.textbox) _rotateTextBox(newElement);
423
 
615
 
424
-    if (element.type == ElementType.textbox) _rotateTextBox(element);
616
+
617
+    setNewElementsState(newElementsState);
425
 
618
 
426
 
619
 
427
     // Adjust Size
620
     // Adjust Size
436
 
629
 
437
   }
630
   }
438
 
631
 
439
-  void _rotateText(ElementProperty element) {
440
-    if (element.quarterTurns < 3) {
441
-      element.quarterTurns += 1;
442
-    } else {
443
-      element.quarterTurns = 0;
444
-    }
632
+  
633
+
634
+
635
+  void editText(TextEditingController controller) {
636
+    if (selectedElm == null) return;
637
+
638
+    List<ElementState> newElementsState = _cloneElementsState(currentElementsState);
639
+    var newElement = newElementsState.firstWhere((e) => e.id == selectedElmId);
640
+
641
+    newElement.valueController.text = controller.text;
642
+    // newElement.valueController.selection = TextSelection.collapsed(offset: controller.selection.base.offset);
643
+    
644
+    setNewElementsState(newElementsState);
445
   }
645
   }
446
 
646
 
447
-  void _rotateTextBox(ElementProperty element) {
448
-    if (element.quarterTurns == 0) {
449
-      element.quarterTurns = 3;
450
-    } else {
451
-      element.quarterTurns = 0;
452
-    }
647
+  void setValueOnStartEditing(String text) {
648
+    valueOnStartEditing = text;
453
   }
649
   }
454
 
650
 
651
+
455
   // FontSize Handler
652
   // FontSize Handler
456
   void changeFontSize(int? fontSize) {
653
   void changeFontSize(int? fontSize) {
457
     if (fontSize == null) return;
654
     if (fontSize == null) return;
463
       return;
660
       return;
464
     }
661
     }
465
 
662
 
466
-    selectedElm!.fontScale = fontSize;
663
+    List<ElementState> newElementsState = _cloneElementsState(currentElementsState);
664
+    var newElement = newElementsState.firstWhere((e) => e.id == selectedElmId);
665
+
666
+    newElement.fontScale = fontSize;
667
+
668
+    setNewElementsState(newElementsState);
669
+
467
     notifyListeners();
670
     notifyListeners();
468
   }
671
   }
469
   
672
   
478
     }
681
     }
479
     // check if value is allowed for resize
682
     // check if value is allowed for resize
480
     if (CanvasStyle.fontSizeMap.containsKey(incrementTo)) {
683
     if (CanvasStyle.fontSizeMap.containsKey(incrementTo)) {
481
-      selectedElm!.fontScale = incrementTo;
684
+      // // ? Save History
685
+      // setNewElementsState(CanvasHistoryModifyType.resize, elementProperties);
686
+
687
+      List<ElementState> newElementsState = _cloneElementsState(currentElementsState);
688
+      var newElement = newElementsState.firstWhere((e) => e.id == selectedElmId);
689
+
690
+      // selectedElm!.fontScale = incrementTo;
691
+      newElement.fontScale = incrementTo;
692
+
693
+      setNewElementsState(newElementsState);
482
       print('kepenjet increase');
694
       print('kepenjet increase');
483
     } else {
695
     } else {
484
       print('cant increment');
696
       print('cant increment');
498
     }
710
     }
499
     // check if value is allowed for resize
711
     // check if value is allowed for resize
500
     if (CanvasStyle.fontSizeMap.containsKey(decrementTo)) {
712
     if (CanvasStyle.fontSizeMap.containsKey(decrementTo)) {
501
-      selectedElm!.fontScale = decrementTo;
713
+      List<ElementState> newElementsState = _cloneElementsState(currentElementsState);
714
+      var newElement = newElementsState.firstWhere((e) => e.id == selectedElmId);
715
+
716
+      newElement.fontScale = decrementTo;
717
+
718
+      setNewElementsState(newElementsState);
502
     } else {
719
     } else {
503
       print('cant decrement');
720
       print('cant decrement');
504
     }
721
     }
505
-
506
-    notifyListeners();
507
   }
722
   }
508
 
723
 
509
   // Qr Size Handler
724
   // Qr Size Handler
517
       return;
732
       return;
518
     }
733
     }
519
 
734
 
520
-    selectedElm!.qrScale = fontSize;
521
-    notifyListeners();
735
+    List<ElementState> newElementsState = _cloneElementsState(currentElementsState);
736
+    var newElement = newElementsState.firstWhere((e) => e.id == selectedElmId);
737
+
738
+    // selectedElm!.qrScale = fontSize;
739
+    newElement.qrScale = fontSize;
740
+
741
+    setNewElementsState(newElementsState);
522
   }
742
   }
523
   
743
   
524
   void incrementQrSize() {
744
   void incrementQrSize() {
534
     }
754
     }
535
     // check if value is allowed for resize
755
     // check if value is allowed for resize
536
     if (CanvasStyle.qrSizeMap.containsKey(incrementTo)) {
756
     if (CanvasStyle.qrSizeMap.containsKey(incrementTo)) {
537
-      selectedElm!.qrScale = incrementTo;
757
+      List<ElementState> newElementsState = _cloneElementsState(currentElementsState);
758
+      var newElement = newElementsState.firstWhere((e) => e.id == selectedElmId);
759
+
760
+      // selectedElm!.qrScale = incrementTo;
761
+      newElement.qrScale = incrementTo;
762
+
763
+      setNewElementsState(newElementsState);
538
     } else {
764
     } else {
539
       print('cant increment');
765
       print('cant increment');
540
     }
766
     }
541
 
767
 
542
-    notifyListeners();
543
   }
768
   }
544
 
769
 
545
   void decrementQrSize() {
770
   void decrementQrSize() {
557
 
782
 
558
     // check if value is allowed for resize
783
     // check if value is allowed for resize
559
     if (CanvasStyle.qrSizeMap.containsKey(decrementTo)) {
784
     if (CanvasStyle.qrSizeMap.containsKey(decrementTo)) {
560
-      selectedElm!.qrScale = decrementTo;
785
+      List<ElementState> newElementsState = _cloneElementsState(currentElementsState);
786
+      var newElement = newElementsState.firstWhere((e) => e.id == selectedElmId);
787
+
788
+      // selectedElm!.qrScale = decrementTo;
789
+      newElement.qrScale = decrementTo;
790
+
791
+      setNewElementsState(newElementsState);
561
     } else {
792
     } else {
562
       print('cant decrement');
793
       print('cant decrement');
563
     }
794
     }
596
 
827
 
597
     if (!shouldDelete) return;
828
     if (!shouldDelete) return;
598
 
829
 
599
-    elementProperties.removeWhere((e) => e.id == selectedElm!.id);
600
-    unSelectElm();
830
+    // // ? Save History
831
+    // setNewElementsState(CanvasHistoryModifyType.remove, elementProperties);
832
+
833
+    // elementProperties.removeWhere((e) => e.id == selectedElm!.id);
601
 
834
 
835
+    List<ElementState> newElementsState = _cloneElementsState(currentElementsState);
836
+    log('selectedElmId: ${selectedElm!.id}');
837
+
838
+    for (var i = 0; i < newElementsState.length; i++) {
839
+      log('element[$i]: ${newElementsState[i].id}');
840
+    }
841
+    newElementsState.removeWhere((e) => e.id == selectedElm!.id);
842
+
843
+    setNewElementsState(newElementsState);
844
+    
845
+    unSelectElm();
602
     notifyListeners();
846
     notifyListeners();
603
   }
847
   }
604
 
848
 
610
   //   }
854
   //   }
611
   // }
855
   // }
612
 
856
 
613
-  // ? helper method
614
-  void _showLockedToast(String titleText) {
615
-    toastification.show(
616
-      title: Text(titleText),
617
-      description: Text('unlock to change element property'),
618
-      closeButtonShowType: CloseButtonShowType.none,
619
-      style: ToastificationStyle.minimal,
620
-      type: ToastificationType.warning,
621
-      autoCloseDuration: const Duration(seconds: 3),
622
-      alignment: Alignment.bottomCenter,
623
-      dragToClose: true
624
-    );
625
-  }
626
-
627
-
628
-
629
 
857
 
630
   // ? Template to JSON
858
   // ? Template to JSON
631
   String buildJSON() {
859
   String buildJSON() {
642
       }
870
       }
643
     };
871
     };
644
 
872
 
645
-    for (var element in elementProperties) {
873
+    for (var element in currentElementsState) {
646
       var elementMap = {
874
       var elementMap = {
647
-        'id': uuid.v4(),
875
+        'id': element.id,
648
         'content': element.valueController.text,
876
         'content': element.valueController.text,
649
         'height': element.elementKey.currentContext?.size?.height.round() ?? 0,
877
         'height': element.elementKey.currentContext?.size?.height.round() ?? 0,
650
         'width': element.elementKey.currentContext?.size?.width.round() ?? 0,
878
         'width': element.elementKey.currentContext?.size?.width.round() ?? 0,
673
   }
901
   }
674
 
902
 
675
 
903
 
676
-  String _getElementTypeResult(ElementProperty element) {
904
+  String _getElementTypeResult(ElementState element) {
677
     switch (element.type) {
905
     switch (element.type) {
678
       case ElementType.text:
906
       case ElementType.text:
679
         return 'text';
907
         return 'text';
699
     }
927
     }
700
   }
928
   }
701
 
929
 
702
-  int _getElementSizeResult(ElementProperty element) {
930
+  int _getElementSizeResult(ElementState element) {
703
     if (element.type == ElementType.qr) {
931
     if (element.type == ElementType.qr) {
704
       return element.qrScale ?? 1;
932
       return element.qrScale ?? 1;
705
     }
933
     }
706
 
934
 
707
     return element.fontScale;
935
     return element.fontScale;
708
   }
936
   }
709
-}
937
+
938
+
939
+
940
+
941
+
942
+
943
+  // ? Helper Method
944
+
945
+  void _setAddNewElementState(ElementState newElement) {
946
+    List<ElementState> newElementsState = [..._cloneElementsState(currentElementsState), newElement];
947
+    setNewElementsState(newElementsState);
948
+  }
949
+
950
+  void _rotateText(ElementState element) {
951
+    if (element.quarterTurns < 3) {
952
+      element.quarterTurns += 1;
953
+    } else {
954
+      element.quarterTurns = 0;
955
+    }
956
+  }
957
+
958
+  void _rotateTextBox(ElementState element) {
959
+    if (element.quarterTurns == 0) {
960
+      element.quarterTurns = 3;
961
+    } else {
962
+      element.quarterTurns = 0;
963
+    }
964
+  }
965
+
966
+
967
+  void _showLockedToast(String titleText) {
968
+    toastification.show(
969
+      title: Text(titleText),
970
+      description: Text('unlock to change element property'),
971
+      closeButtonShowType: CloseButtonShowType.none,
972
+      style: ToastificationStyle.minimal,
973
+      type: ToastificationType.warning,
974
+      autoCloseDuration: const Duration(seconds: 3),
975
+      alignment: Alignment.bottomCenter,
976
+      dragToClose: true
977
+    );
978
+  }
979
+
980
+  void _setNewStateTextEdit(List<ElementState> newElementsState) {
981
+    log(selectedElm?.id ?? 'null');
982
+    if (selectedElm == null) return;
983
+    
984
+    
985
+    var newElement = newElementsState.firstWhereOrNull((e) => e.id == selectedElmId);
986
+
987
+    if (newElement != null && [ElementType.text, ElementType.textbox].contains(newElement.type)) {
988
+      newElement.valueController.selection = TextSelection.collapsed(offset: selectedElm!.valueController.selection.base.offset);
989
+    }
990
+  }
991
+}
992
+
993
+
994
+enum SetActionType { add, remove, move, resize, lock, rotate, textEdit, redo }

+ 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
+}

+ 32 - 0
lib/utils/debouncer.dart

1
+import 'dart:async';
2
+
3
+class Debouncer {
4
+  static Timer? _timer;
5
+
6
+  static void run(void Function() callback, [Duration duration = const Duration(milliseconds: 500)]) {
7
+    if (_timer?.isActive ?? false) {
8
+      _timer!.cancel();
9
+    
10
+    }
11
+
12
+    _timer = Timer(duration, callback);
13
+  }
14
+}
15
+
16
+
17
+void main() {
18
+  // Debouncer debouncer = Debouncer(Duration(seconds: 5));
19
+  print('program running');
20
+
21
+
22
+  // debouncer.run(() {
23
+  //   print('Terdebounce');
24
+  // });
25
+
26
+  
27
+
28
+  Debouncer.run(
29
+    () => print('terdebounce'),
30
+    Duration(seconds: 5)
31
+  );
32
+}

+ 377 - 286
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';
10
+import 'package:flutter_canvas_editor/utils/debouncer.dart';
5
 import 'package:intl/intl.dart';
11
 import 'package:intl/intl.dart';
6
 import 'package:provider/provider.dart';
12
 import 'package:provider/provider.dart';
7
 import 'dart:math' as math;
13
 import 'dart:math' as math;
62
   ElementSize({required this.width, required this.height});
68
   ElementSize({required this.width, required this.height});
63
 }
69
 }
64
 
70
 
65
-class ElementProperty {
71
+class ElementState {
66
   String id;
72
   String id;
67
   TextEditingController valueController;
73
   TextEditingController valueController;
68
   ElementType type;
74
   ElementType type;
76
   int fontScale;
82
   int fontScale;
77
   int? qrScale; 
83
   int? qrScale; 
78
   bool isLocked;
84
   bool isLocked;
85
+  String? lastSavedText;
79
 
86
 
80
-  ElementProperty({
87
+  ElementState({
81
     required this.id,
88
     required this.id,
82
     required this.valueController,
89
     required this.valueController,
83
     required this.type,
90
     required this.type,
90
     this.fontScale = 3,
97
     this.fontScale = 3,
91
     this.qrScale,
98
     this.qrScale,
92
     this.isLocked = false,
99
     this.isLocked = false,
93
-    this.variableType
100
+    this.variableType,
101
+    this.lastSavedText
94
   });
102
   });
95
 
103
 
96
 }
104
 }
98
 
106
 
99
 
107
 
100
 class ElementWidget extends StatefulWidget {
108
 class ElementWidget extends StatefulWidget {
101
-  final ElementProperty elementProperty;
109
+  final ElementState elementProperty;
102
   final CanvasProperty canvasProperty;
110
   final CanvasProperty canvasProperty;
103
   final GlobalKey globalKey;
111
   final GlobalKey globalKey;
104
   
112
   
114
 }
122
 }
115
 
123
 
116
 class _ElementWidgetState extends State<ElementWidget> {
124
 class _ElementWidgetState extends State<ElementWidget> {
117
-  Offset? _dragStartPosition;
125
+  Offset? _textboxDragStartPosition;
126
+  double? _textboxDragStartWidth;
127
+
128
+  ElementPosition? _elementDragStartPosition;
129
+  // String? _valueOnStartEditing;
118
 
130
 
119
   // dragable container 
131
   // dragable container 
120
   final double _resizerHeight = 36;
132
   final double _resizerHeight = 36;
125
   double _currentScale = 1.0;
137
   double _currentScale = 1.0;
126
 
138
 
127
   late TransformationController _transformController;
139
   late TransformationController _transformController;
140
+  
141
+  // // TODO: pindhkan ke state element property agar bisa dipake di widget Toolbar
142
+  // String? _lastSavedText;
143
+
144
+  FocusNode? _focus = FocusNode();
128
 
145
 
129
   @override
146
   @override
130
   void initState() {
147
   void initState() {
131
     super.initState();
148
     super.initState();
132
     
149
     
150
+    // _valueOnStartEditing = widget.elementProperty.valueController.text;
151
+
133
     WidgetsBinding.instance.addPostFrameCallback((_) => _updateScale);
152
     WidgetsBinding.instance.addPostFrameCallback((_) => _updateScale);
134
 
153
 
135
     _transformController = Provider.of<Editor>(context, listen: false).canvasTransformationController;
154
     _transformController = Provider.of<Editor>(context, listen: false).canvasTransformationController;
136
 
155
 
137
     _setInitialScale();
156
     _setInitialScale();
138
     _listenTransformationChanges();
157
     _listenTransformationChanges();
158
+    _addFocusNodeListener();
159
+
160
+    widget.elementProperty.valueController.addListener(() {});
139
   }
161
   }
140
 
162
 
141
 
163
 
158
     _transformController.removeListener(_updateScale);
180
     _transformController.removeListener(_updateScale);
159
   }
181
   }
160
 
182
 
183
+  void _addFocusNodeListener() {
184
+    if ([ElementType.text, ElementType.textbox].contains(widget.elementProperty.type)) {
185
+      _focus!.addListener(_onTextFieldFocusChange);
186
+    }
187
+  }
188
+
189
+  void _onTextFieldFocusChange() {
190
+    if (widget.elementProperty.lastSavedText != widget.elementProperty.valueController.text) {
191
+      print('executed');
192
+      // Provider.of<Editor>(context, listen: false).setNewElementsState(CanvasHistoryModifyType.textEdit, Provider.of<Editor>(context, listen:  false).elementProperties);
193
+
194
+
195
+      widget.elementProperty.lastSavedText = widget.elementProperty.valueController.text;
196
+    }
197
+  }
198
+
199
+  void _removeFocusNodeListener() {
200
+    if ([ElementType.text, ElementType.textbox].contains(widget.elementProperty.type)) {
201
+      _focus!.removeListener(_onTextFieldFocusChange);
202
+    }
203
+  }
204
+
161
 
205
 
162
 
206
 
163
 
207
 
164
   @override
208
   @override
165
   void dispose() {
209
   void dispose() {
166
    _removeTransformationListener();
210
    _removeTransformationListener();
211
+   _removeFocusNodeListener();
167
     super.dispose();
212
     super.dispose();
168
   }
213
   }
169
 
214
 
172
   Widget build(BuildContext context) {
217
   Widget build(BuildContext context) {
173
     final editorProvider = Provider.of<Editor>(context);
218
     final editorProvider = Provider.of<Editor>(context);
174
 
219
 
175
-    ElementProperty element = widget.elementProperty;
220
+    ElementState element = widget.elementProperty;
176
     final CanvasProperty canvas = widget.canvasProperty;
221
     final CanvasProperty canvas = widget.canvasProperty;
177
 
222
 
178
     WidgetsBinding.instance.addPostFrameCallback((_) {
223
     WidgetsBinding.instance.addPostFrameCallback((_) {
179
       _height = element.elementKey.currentContext!.size!.height;
224
       _height = element.elementKey.currentContext!.size!.height;
180
     });
225
     });
181
 
226
 
227
+    // List<ElementState> currentCanvasState = editorProvider.elementProperties;
228
+
182
     return Positioned(
229
     return Positioned(
183
       top: element.position.top,
230
       top: element.position.top,
184
       left: element.position.left,
231
       left: element.position.left,
185
-      child: GestureDetector(
186
-        onDoubleTap: () {
187
-          print('double tap detected');
188
-          Provider.of<Editor>(context, listen: false).selectElmById(element.id);
189
-          Provider.of<Editor>(context, listen: false).enableEdit();
190
-        },
191
-        onTap: () {
192
-          print('Element Gesture Detector Tapped!');
193
-          Provider.of<Editor>(context, listen: false).selectElmById(element.id);
194
-        },
195
-        onPanUpdate: (details) {
196
-          if (!Provider.of<Editor>(context, listen: false).isSelected(element.id)) {
197
-            return;
198
-          }
199
-
200
-          if (element.isLocked) return;
201
-    
202
-          final elmKeyContext = Provider.of<Editor>(context, listen: false).selectedElmKey?.currentContext;
203
-    
204
-          if (elmKeyContext == null) {
205
-            debugPrint('WARNING, elmKeyContext not found');
206
-          }
207
-    
208
-          bool isElmRotatedVertically = [1,3].contains(element.quarterTurns);
209
-    
210
-          double width = isElmRotatedVertically ? elmKeyContext!.size!.height : elmKeyContext!.size!.width;
211
-          double height = isElmRotatedVertically ? elmKeyContext.size!.width : elmKeyContext.size!.height;
212
-    
213
-          double right = element.position.left + width;
214
-          double bottom = element.position.top + height;
215
-          
216
-    
217
-          bool isElmWidthExceedCanvas = width > canvas.width; 
218
-          bool isElmHeightExceedCanvas = height > canvas.height;
219
-      
220
-          // Check if the object is out of the canvas
221
-          if (element.position.top < 0) {
222
-            setState(() {
223
-              element.position.top = 0;
224
-            });
225
-      
226
-            print('object is out of canvas');
227
-            return;
228
-          }
229
-      
230
-          if (element.position.left < 0) {
231
-            setState(() {
232
-              element.position.left = 0;
233
-            });
234
-      
235
-            print('object is out of canvas');
236
-            return;
237
-          }
238
-    
239
-          if (!isElmHeightExceedCanvas && bottom > canvas.height) {
240
-            setState(() {
241
-              element.position.top = (canvas.height - height).roundToDouble() - 1;
242
-            });
243
-      
244
-            print('object is out of canvas');
245
-            return;
246
-          }
247
-      
248
-      
249
-          if (!isElmWidthExceedCanvas && right > canvas.width) {
250
-            setState(() {
251
-              element.position.left = (canvas.width - width).roundToDouble() - 1;
252
-            });
253
-      
254
-            print('object is out of canvas');
255
-            return;
256
-          }
257
-      
258
-          Provider.of<Editor>(context, listen: false).updateElmPosition(details.delta);
259
-        },
260
-        onPanEnd: (details) {
261
-          // ? Adjust overflow position
262
-          ElementPosition adjustedElementPosition = element.position;
263
-          final elmKeyContext = element.elementKey.currentContext;
264
-
265
-          bool isElmRotatedVertically = [1,3].contains(element.quarterTurns);
266
-    
267
-          double width = isElmRotatedVertically ? elmKeyContext!.size!.height : elmKeyContext!.size!.width;
268
-          double height = isElmRotatedVertically ? elmKeyContext.size!.width : elmKeyContext.size!.height;
269
-    
270
-          double right = element.position.left + width;
271
-          double bottom = element.position.top + height;
272
-
273
-
274
-          if (element.position.top < 0) {
275
-              adjustedElementPosition.top = 0;
276
-          }
277
-
278
-          if (element.position.left < 0) {
279
-              adjustedElementPosition.left = 0;
280
-          }
281
-
282
-          if ((element.position.top + height) > canvas.height) {
283
-              adjustedElementPosition.top = (canvas.height - height).roundToDouble() - 1;
284
-          }
285
-
286
-          if ((element.position.left + width) > canvas.width) {
287
-              adjustedElementPosition.left = (canvas.width - width).roundToDouble() - 1;
232
+      child: IgnorePointer(
233
+        ignoring: editorProvider.shouldIgnoreTouch(element.id),
234
+        child: GestureDetector(
235
+          onDoubleTap: () {
236
+            print('double tap detected');
237
+            Provider.of<Editor>(context, listen: false).selectElmById(element.id);
238
+            Provider.of<Editor>(context, listen: false).enableEdit();
239
+          },
240
+          onTap: () {
241
+            print('Element Gesture Detector Tapped!');
242
+            Provider.of<Editor>(context, listen: false).selectElmById(element.id);
243
+          },
244
+          onPanStart: (details) {
245
+            if (!Provider.of<Editor>(context, listen: false).isSelected(element.id)) {
246
+              return;
288
             }
247
             }
289
-
290
-
291
-          Provider.of<Editor>(context, listen: false).updateElmPosition(Offset(adjustedElementPosition.left - element.position.left, adjustedElementPosition.top - element.position.top));
292
-        },
293
-        child: Stack(
294
-          clipBehavior: Clip.none,
295
-          children: [
296
-            RotatedBox(
297
-              // angle: element.quarterTurns * (math.pi / 2),
298
-              quarterTurns: element.quarterTurns,
299
-              child: Stack(
300
-                clipBehavior: Clip.none,
301
-                children: [
302
-                  Container(
303
-                    width: element.type == ElementType.text 
304
-                      ? null 
248
+        
249
+            if (element.isLocked) return;
250
+        
251
+            log('Pan Start');
252
+            // inspect(editorProvider.elementProperties);
253
+            // editorProvider.setNewElementsState(CanvasHistoryModifyType.move, editorProvider.elementProperties);
254
+        
255
+            _elementDragStartPosition = Provider.of<Editor>(context, listen: false).getClonedElementPosition(element);
256
+          },
257
+          onPanUpdate: (details) {
258
+            if (!Provider.of<Editor>(context, listen: false).isSelected(element.id)) {
259
+              return;
260
+            }
261
+        
262
+            if (element.isLocked) return;
305
             
263
             
306
-                      : element.type == ElementType.qr 
307
-                      ? CanvasStyle.getQrSize(element.qrScale!)
264
+            final elmKeyContext = Provider.of<Editor>(context, listen: false).selectedElmKey?.currentContext;
308
             
265
             
309
-                      : element.width ,
310
-                    height: element.type == ElementType.qr 
311
-                      ? CanvasStyle.getQrSize(element.qrScale!)
312
-                      : null,
313
-                    // child: Text('Top: ${element.position.top}, Left: ${element.position.left}, isSelected: ${Provider.of<Editor>(context, listen: false).isSelected(element.id)}'),
314
-                    key: widget.globalKey,
315
-                    decoration: BoxDecoration(
316
-                      // color: element.color ?? Colors.blue,
317
-                      border: Provider.of<Editor>(context, listen: true).isSelected(element.id) ? Border.all(
318
-                        color:Colors.red,
319
-                        width: 2,
320
-                        strokeAlign: BorderSide.strokeAlignOutside,
321
-                      ) : null,
322
-                    ),
323
-                    // ? child element
324
-                    child: _buildChildElement(element),
325
-                  ),
266
+            if (elmKeyContext == null) {
267
+              debugPrint('WARNING, elmKeyContext not found');
268
+            }
326
             
269
             
327
-                  // ? Textbox Resizer
328
-                  if (editorProvider.shouldShowTextboxResizer(element.id)) Positioned(
329
-                    right: _resizerWidth / -2,
330
-                    top:_height / 2 - (_resizerHeight / 2),
331
-                    child: DeferPointer(
332
-                      link: editorProvider.textboxResizerDeferredPointerHandlerLink,
333
-                      // paintOnTop: true,
334
-                      child: Transform.scale(
335
-                        scale: 1 / _currentScale,
336
-                        child: Listener(
337
-                          onPointerDown: (details) {
338
-                            _dragStartPosition = details.position;
339
-                          },
340
-                          onPointerMove: (details) {
341
-                            if (element.isLocked) return;
342
-                        
343
-                            setState(() {
344
-                              final selectedElm = Provider.of<Editor>(context, listen: false).selectedElm;
345
-                        
346
-                              if (selectedElm == null) return;
347
-                        
348
-                        
349
-                              final elmKeyContext = selectedElm.elementKey.currentContext;
350
-                        
351
-                              double width = elmKeyContext!.size!.width;
352
-                        
353
-                              print(MediaQuery.of(context).devicePixelRatio);
354
-                                    
355
-                                    
356
-                              var delta;
357
-                              double canvasWidth = 1.0;
358
-                                    
359
-                              print (_dragStartPosition!.dx);
360
-                              print (_dragStartPosition!.dy);
361
-                                    
362
-                              // adjust width based on rotation
363
-                              print('quarter turn: ${element.quarterTurns}');
364
-                              switch(element.quarterTurns) {
365
-                                case 0:
366
-                                  delta = details.position.dx - _dragStartPosition!.dx;
367
-                                  canvasWidth = Provider.of<Editor>(context, listen: false).canvasProperty.width;
368
-                                case 3:
369
-                                  delta = _dragStartPosition!.dy - details.position.dy;
370
-                                  element.position.top -= delta / _currentScale;
371
-                                  canvasWidth = Provider.of<Editor>(context, listen: false).canvasProperty.height;
372
-                              }
373
-                              
374
-                              element.width += delta / _currentScale; // Adjust width
375
-                              print('current scale $_currentScale');
376
-                        
377
-                              // Enforce minimum size
378
-                              element.width = element.width.clamp(75.0, canvasWidth);
379
-                               
380
-                              _dragStartPosition = details.position;
381
-                              if (width <= 0) return;
382
-                        
383
-                              Provider.of<Editor>(context, listen: false).updateElmWitdh(element.width);
384
-                            });
385
-                          },
386
-                          onPointerUp: (details) {
387
-                            _dragStartPosition = null;
388
-                          },
389
-                          child: Container(
390
-                            decoration: BoxDecoration(
391
-                              shape: BoxShape.circle,
392
-                              color: Colors.blue.withOpacity(0.7),
393
-                            ),
394
-                            width: _resizerWidth,
395
-                            height: _resizerHeight,
396
-                            child: const RotatedBox(
397
-                              quarterTurns: 1,
398
-                              child: Icon(
399
-                                Icons.height,
400
-                                size: 24,
401
-                                color: Colors.white,
270
+            bool isElmRotatedVertically = [1,3].contains(element.quarterTurns);
271
+            
272
+            double width = isElmRotatedVertically ? elmKeyContext!.size!.height : elmKeyContext!.size!.width;
273
+            double height = isElmRotatedVertically ? elmKeyContext.size!.width : elmKeyContext.size!.height;
274
+            
275
+            double right = element.position.left + width;
276
+            double bottom = element.position.top + height;
277
+            
278
+            
279
+            bool isElmWidthExceedCanvas = width > canvas.width; 
280
+            bool isElmHeightExceedCanvas = height > canvas.height;
281
+        
282
+            // Check if the object is out of the canvas
283
+            if (element.position.top < 0) {
284
+              setState(() {
285
+                element.position.top = 0;
286
+              });
287
+        
288
+              print('object is out of canvas');
289
+              return;
290
+            }
291
+        
292
+            if (element.position.left < 0) {
293
+              setState(() {
294
+                element.position.left = 0;
295
+              });
296
+        
297
+              print('object is out of canvas');
298
+              return;
299
+            }
300
+            
301
+            if (!isElmHeightExceedCanvas && bottom > canvas.height) {
302
+              setState(() {
303
+                element.position.top = (canvas.height - height).roundToDouble() - 1;
304
+              });
305
+        
306
+              print('object is out of canvas');
307
+              return;
308
+            }
309
+        
310
+        
311
+            if (!isElmWidthExceedCanvas && right > canvas.width) {
312
+              setState(() {
313
+                element.position.left = (canvas.width - width).roundToDouble() - 1;
314
+              });
315
+        
316
+              print('object is out of canvas');
317
+              return;
318
+            }
319
+        
320
+            Provider.of<Editor>(context, listen: false).updateElmPosition(details.delta);
321
+          },
322
+          onPanEnd: (details) {
323
+            if (!Provider.of<Editor>(context, listen: false).isSelected(element.id)) {
324
+              return;
325
+            }
326
+        
327
+            if (element.isLocked) return;
328
+            
329
+        
330
+            // ? Adjust overflow position
331
+            ElementPosition adjustedElementPosition = element.position;
332
+            final elmKeyContext = element.elementKey.currentContext;
333
+        
334
+            bool isElmRotatedVertically = [1,3].contains(element.quarterTurns);
335
+            
336
+            double width = isElmRotatedVertically ? elmKeyContext!.size!.height : elmKeyContext!.size!.width;
337
+            double height = isElmRotatedVertically ? elmKeyContext.size!.width : elmKeyContext.size!.height;
338
+            
339
+            double right = element.position.left + width;
340
+            double bottom = element.position.top + height;
341
+        
342
+        
343
+            if (element.position.top < 0) {
344
+                adjustedElementPosition.top = 0;
345
+            }
346
+        
347
+            if (element.position.left < 0) {
348
+                adjustedElementPosition.left = 0;
349
+            }
350
+        
351
+            if ((element.position.top + height) > canvas.height) {
352
+                adjustedElementPosition.top = (canvas.height - height).roundToDouble() - 1;
353
+            }
354
+        
355
+            if ((element.position.left + width) > canvas.width) {
356
+                adjustedElementPosition.left = (canvas.width - width).roundToDouble() - 1;
357
+              }
358
+        
359
+        
360
+            // Provider.of<Editor>(context, listen: false).updateElmPosition(
361
+            //   Offset(adjustedElementPosition.left - element.position.left, adjustedElementPosition.top - element.position.top),
362
+            //   // Provider.of<Editor>(context, listen: false).elementProperties
363
+            // );
364
+        
365
+            Provider.of<Editor>(context, listen: false).resetElmPosition(_elementDragStartPosition);
366
+        
367
+            Provider.of<Editor>(context, listen: false).moveElement(
368
+              Offset(adjustedElementPosition.left - element.position.left, adjustedElementPosition.top - element.position.top)
369
+            );
370
+          },
371
+          child: Stack(
372
+            clipBehavior: Clip.none,
373
+            children: [
374
+              RotatedBox(
375
+                // angle: element.quarterTurns * (math.pi / 2),
376
+                quarterTurns: element.quarterTurns,
377
+                child: Stack(
378
+                  clipBehavior: Clip.none,
379
+                  children: [
380
+                    Container(
381
+                      width: element.type == ElementType.text 
382
+                        ? null 
383
+              
384
+                        : element.type == ElementType.qr 
385
+                        ? CanvasStyle.getQrSize(element.qrScale!)
386
+              
387
+                        : element.width ,
388
+                      height: element.type == ElementType.qr 
389
+                        ? CanvasStyle.getQrSize(element.qrScale!)
390
+                        : null,
391
+                      // child: Text('Top: ${element.position.top}, Left: ${element.position.left}, isSelected: ${Provider.of<Editor>(context, listen: false).isSelected(element.id)}'),
392
+                      key: widget.globalKey,
393
+                      decoration: BoxDecoration(
394
+                        // color: element.color ?? Colors.blue,
395
+                        border: Provider.of<Editor>(context, listen: true).isSelected(element.id) ? Border.all(
396
+                          color:Colors.red,
397
+                          width: 2,
398
+                          strokeAlign: BorderSide.strokeAlignOutside,
399
+                        ) : null,
400
+                      ),
401
+                      // ? child element
402
+                      child: _buildChildElement(element),
403
+                    ),
404
+              
405
+                    // ? Textbox Resizer
406
+                    if (editorProvider.shouldShowTextboxResizer(element.id)) Positioned(
407
+                      right: _resizerWidth / -2,
408
+                      top:_height / 2 - (_resizerHeight / 2),
409
+                      child: DeferPointer(
410
+                        link: editorProvider.textboxResizerDeferredPointerHandlerLink,
411
+                        // paintOnTop: true,
412
+                        child: Transform.scale(
413
+                          scale: 1 / _currentScale,
414
+                          child: Listener(
415
+                            onPointerDown: (details) {
416
+                              _textboxDragStartPosition = details.position;
417
+                              _textboxDragStartWidth = element.width;
418
+                            },
419
+                            onPointerMove: (details) {
420
+                              if (element.isLocked) return;
421
+                          
422
+                              setState(() {
423
+                                final selectedElm = Provider.of<Editor>(context, listen: false).selectedElm;
424
+                          
425
+                                if (selectedElm == null) return;
426
+                          
427
+                          
428
+                                final elmKeyContext = selectedElm.elementKey.currentContext;
429
+                          
430
+                                double width = elmKeyContext!.size!.width;
431
+                          
432
+                                print(MediaQuery.of(context).devicePixelRatio);
433
+                                      
434
+                                      
435
+                                var delta;
436
+                                double canvasWidth = 1.0;
437
+                                      
438
+                                print (_textboxDragStartPosition!.dx);
439
+                                print (_textboxDragStartPosition!.dy);
440
+                                      
441
+                                // adjust width based on rotation
442
+                                print('quarter turn: ${element.quarterTurns}');
443
+                                switch(element.quarterTurns) {
444
+                                  case 0:
445
+                                    delta = details.position.dx - _textboxDragStartPosition!.dx;
446
+                                    canvasWidth = Provider.of<Editor>(context, listen: false).canvasProperty.width;
447
+                                  case 3:
448
+                                    delta = _textboxDragStartPosition!.dy - details.position.dy;
449
+                                    element.position.top -= delta / _currentScale;
450
+                                    canvasWidth = Provider.of<Editor>(context, listen: false).canvasProperty.height;
451
+                                }
452
+                                
453
+                                element.width += delta / _currentScale; // Adjust width
454
+                                print('current scale $_currentScale');
455
+                          
456
+                                // Enforce minimum size
457
+                                element.width = element.width.clamp(75.0, canvasWidth);
458
+                                 
459
+                                _textboxDragStartPosition = details.position;
460
+                                if (width <= 0) return;
461
+                          
462
+                                Provider.of<Editor>(context, listen: false).updateElmWitdh(element.width);
463
+                              });
464
+                            },
465
+                            onPointerUp: (details) {
466
+                              editorProvider.commitTextboxResize(element.width, _textboxDragStartWidth ?? 70);
467
+        
468
+                              _textboxDragStartPosition = null;
469
+                              _textboxDragStartWidth = null;
470
+                            },
471
+                            child: Container(
472
+                              decoration: BoxDecoration(
473
+                                shape: BoxShape.circle,
474
+                                color: Colors.blue.withOpacity(0.7),
475
+                              ),
476
+                              width: _resizerWidth,
477
+                              height: _resizerHeight,
478
+                              child: const RotatedBox(
479
+                                quarterTurns: 1,
480
+                                child: Icon(
481
+                                  Icons.height,
482
+                                  size: 24,
483
+                                  color: Colors.white,
484
+                                ),
402
                               ),
485
                               ),
403
                             ),
486
                             ),
404
                           ),
487
                           ),
405
                         ),
488
                         ),
406
                       ),
489
                       ),
407
                     ),
490
                     ),
408
-                  ),
409
-            
410
-            
411
-                  
412
-            
413
-            
414
-            
415
-                  // if (Provider.of<Editor>(context).selectedElmId == element.id && element.type != ElementType.qr) ... [
416
-                    
491
+              
492
+              
417
                     
493
                     
418
-                  // ]
419
-                ],
494
+              
495
+              
496
+              
497
+                    // if (Provider.of<Editor>(context).selectedElmId == element.id && element.type != ElementType.qr) ... [
498
+                      
499
+                      
500
+                    // ]
501
+                  ],
502
+                ),
420
               ),
503
               ),
421
-            ),
422
-
423
-            //? Overlay Button
424
-            if (editorProvider.shouldShowOverlay(element.id)) Positioned(
425
-              top: -60 / _currentScale,
426
-              left: 0,
427
-              child: DeferPointer(
428
-                paintOnTop: true,
429
-                child: Transform.scale(
430
-                  scale: 1 / _currentScale,
431
-                  alignment: Alignment.topLeft,
432
-                  child: Row(
433
-                    children: [
434
-                      IconButton.filled(
435
-                        onPressed: () {
436
-                          print('delete overlay tapped');
437
-                          editorProvider.deleteElement(context);
438
-                        }, 
439
-                        icon: const Icon(Icons.delete),
440
-                        color: Theme.of(context).colorScheme.error,
441
-                        style: ButtonStyle(
442
-                          backgroundColor: WidgetStatePropertyAll(Theme.of(context).colorScheme.errorContainer)
504
+        
505
+              //? Overlay Button
506
+              if (editorProvider.shouldShowOverlay(element.id)) Positioned(
507
+                top: -60 / _currentScale,
508
+                left: 0,
509
+                child: DeferPointer(
510
+                  paintOnTop: true,
511
+                  child: Transform.scale(
512
+                    scale: 1 / _currentScale,
513
+                    alignment: Alignment.topLeft,
514
+                    child: Row(
515
+                      children: [
516
+                        IconButton.filled(
517
+                          onPressed: () {
518
+                            print('delete overlay tapped');
519
+                            editorProvider.deleteElement(context);
520
+                          }, 
521
+                          icon: const Icon(Icons.delete),
522
+                          color: Theme.of(context).colorScheme.error,
523
+                          style: ButtonStyle(
524
+                            backgroundColor: WidgetStatePropertyAll(Theme.of(context).colorScheme.errorContainer)
525
+                          ),
443
                         ),
526
                         ),
444
-                      ),
445
-                      IconButton.filled(
446
-                        onPressed: () {
447
-                          print('rotate overlay tapped');
448
-                          editorProvider.rotate();
449
-                  
450
-                          // test
451
-                          var getBox = element.elementKey.currentContext!.findRenderObject();
452
-                        }, 
453
-                        icon: const Icon(Icons.rotate_90_degrees_cw),
454
-                        // color: Theme.of(context).colorScheme.error,
455
-                        style: const ButtonStyle(
456
-                          // backgroundColor: WidgetStatePropertyAll(Theme.of(context).colorScheme.errorContainer)
527
+                        IconButton.filled(
528
+                          onPressed: () {
529
+                            print('rotate overlay tapped');
530
+                            editorProvider.rotate();
531
+                    
532
+                            // test
533
+                            var getBox = element.elementKey.currentContext!.findRenderObject();
534
+                          }, 
535
+                          icon: const Icon(Icons.rotate_90_degrees_cw),
536
+                          // color: Theme.of(context).colorScheme.error,
537
+                          style: const ButtonStyle(
538
+                            // backgroundColor: WidgetStatePropertyAll(Theme.of(context).colorScheme.errorContainer)
539
+                          ),
457
                         ),
540
                         ),
458
-                      ),
459
-                      IconButton.filled(
460
-                        onPressed: () {
461
-                          print('lock overlay tapped');
462
-                          editorProvider.toggleLockElement();
463
-                        }, 
464
-                        icon: Icon(element.isLocked ? Icons.lock_outline : Icons.lock_open),
465
-                        // color: element.isLocked ? Theme.of(context).colorScheme.error,
466
-                        style: ButtonStyle(
467
-                          backgroundColor: element.isLocked ? WidgetStatePropertyAll(Theme.of(context).colorScheme.error) : null
541
+                        IconButton.filled(
542
+                          onPressed: () {
543
+                            print('lock overlay tapped');
544
+                            editorProvider.toggleLockElement();
545
+                          }, 
546
+                          icon: Icon(element.isLocked ? Icons.lock_outline : Icons.lock_open),
547
+                          // color: element.isLocked ? Theme.of(context).colorScheme.error,
548
+                          style: ButtonStyle(
549
+                            backgroundColor: element.isLocked ? WidgetStatePropertyAll(Theme.of(context).colorScheme.error) : null
550
+                          ),
468
                         ),
551
                         ),
469
-                      ),
470
-                    ],
552
+                      ],
553
+                    ),
471
                   ),
554
                   ),
472
-                ),
555
+                )
473
               )
556
               )
474
-            )
475
-          ],
557
+            ],
558
+          ),
476
         ),
559
         ),
477
       ),
560
       ),
478
     );
561
     );
479
   }
562
   }
480
 
563
 
481
 
564
 
482
-  Widget _buildChildElement(ElementProperty element) {
565
+  Widget _buildChildElement(ElementState element) {
483
     // ? build QR element
566
     // ? build QR element
484
     if (element.type == ElementType.qr) {
567
     if (element.type == ElementType.qr) {
485
       return const Image(image: AssetImage('asset/images/qr_template.png'));
568
       return const Image(image: AssetImage('asset/images/qr_template.png'));
489
     if (Provider.of<Editor>(context, listen: true).isEditing && (Provider.of<Editor>(context, listen: true).selectedElmId == element.id)) {
572
     if (Provider.of<Editor>(context, listen: true).isEditing && (Provider.of<Editor>(context, listen: true).selectedElmId == element.id)) {
490
       return IntrinsicWidth(
573
       return IntrinsicWidth(
491
         child: TextField(
574
         child: TextField(
492
-          controller: Provider.of<Editor>(context).selectedElm!.valueController,
575
+          controller: element.valueController,
493
           autofocus: true,
576
           autofocus: true,
494
           keyboardType: TextInputType.multiline,
577
           keyboardType: TextInputType.multiline,
495
           enableSuggestions: false,
578
           enableSuggestions: false,
501
             contentPadding: EdgeInsets.zero,
584
             contentPadding: EdgeInsets.zero,
502
             border: InputBorder.none,
585
             border: InputBorder.none,
503
           ),
586
           ),
504
-          onChanged: (_) {
505
-            setState(() {
506
-              
587
+          onChanged: (newText) {
588
+
589
+            Debouncer.run(() {
590
+              log('[SAVING TO HISTORY]');
591
+              Provider.of<Editor>(context, listen: false).editText(element.valueController);
592
+              element.valueController.text = Provider.of<Editor>(context, listen: false).valueOnStartEditing ?? '';
593
+
594
+              Provider.of<Editor>(context, listen: false).setValueOnStartEditing(newText);
507
             });
595
             });
508
-          }
596
+
597
+
598
+            setState(() {});
599
+          },
509
         ),
600
         ),
510
       );
601
       );
511
     }
602
     }

+ 150 - 66
lib/widgets/toolbar.dart

1
+import 'dart:developer';
2
+
3
+import 'package:easy_debounce/easy_debounce.dart';
1
 import 'package:flutter/material.dart';
4
 import 'package:flutter/material.dart';
5
+import 'package:flutter_canvas_editor/history.dart';
2
 import 'package:flutter_canvas_editor/providers/editor.dart';
6
 import 'package:flutter_canvas_editor/providers/editor.dart';
3
 import 'package:flutter_canvas_editor/style/canvas_style.dart';
7
 import 'package:flutter_canvas_editor/style/canvas_style.dart';
8
+import 'package:flutter_canvas_editor/utils/debouncer.dart';
4
 import 'package:flutter_canvas_editor/widgets/elements.dart';
9
 import 'package:flutter_canvas_editor/widgets/elements.dart';
5
 import 'package:provider/provider.dart';
10
 import 'package:provider/provider.dart';
6
 
11
 
14
 }
19
 }
15
 
20
 
16
 class _ToolbarWidgetState extends State<ToolbarWidget> {
21
 class _ToolbarWidgetState extends State<ToolbarWidget> {
22
+  
23
+
24
+  FocusNode? _focus = FocusNode();
25
+
26
+  // String? _lastSavedText;
27
+
28
+
29
+  
30
+
31
+  // void _addFocusNodeListener() {
32
+  //   if (Provider.of<Editor>(context, listen: false).selectedElmKey != null && [ElementType.text, ElementType.textbox].contains(Provider.of<Editor>(context, listen: false).selectedElm!.type)) {
33
+  //     _focus!.addListener(_onTextFieldFocusChange);
34
+  //   }
35
+  // }
36
+
37
+  // void _onTextFieldFocusChange() {
38
+  //   print('toolbar onfocus change');
39
+
40
+  //   if (Provider.of<Editor>(context, listen: false).selectedElm?.lastSavedText != Provider.of<Editor>(context, listen: false).selectedElm?.valueController.text) {
41
+  //     // Provider.of<Editor>(context, listen: false).setNewElementsState(CanvasHistoryModifyType.textEdit, Provider.of<Editor>(context, listen: false).elementProperties);
42
+  //     Provider.of<Editor>(context, listen: false).selectedElm?.lastSavedText = Provider.of<Editor>(context, listen: false).selectedElm?.valueController.text;
43
+  //   }
44
+  // }
45
+
46
+  // void _removeFocusNodeListener() {
47
+  //   if ([ElementType.text, ElementType.textbox].contains(Provider.of<Editor>(context).selectedElm!.type)) {
48
+  //     _focus!.removeListener(_onTextFieldFocusChange);
49
+  //   }
50
+  // }
51
+
52
+
53
+  @override
54
+  void dispose() {
55
+    // TODO: implement dispose
56
+
57
+    // _removeFocusNodeListener();
58
+    super.dispose();
59
+  }
60
+
61
+
62
+  @override
63
+  Widget build(BuildContext context) {
64
+    final editorProvider = Provider.of<Editor>(context);
65
+
66
+    return Container(
67
+      padding: EdgeInsets.symmetric(horizontal: 16),
68
+      color: Colors.white,
69
+      width: MediaQuery.of(context).size.width,
70
+      height: 150,
71
+      child: Provider.of<Editor>(context).insertElementMode ? insertElementSection() : ToolbarPropertiesWidget(editorProvider: editorProvider),
72
+      // child: ListView(
73
+      //   children: Provider.of<Editor>(context).insertElementMode ? insertElementSection() : elementPropertiesSection(editorProvider),
74
+      // ),
75
+    );
76
+  }
77
+
78
+  Widget insertElementSection() {
79
+    return ListView(
80
+      children: [
81
+        Text('Insert Element'),
82
+        SizedBox(height: 20),
83
+        ElevatedButton(
84
+          onPressed: Provider.of<Editor>(context, listen: false).addTextElement, 
85
+          child: Text('Add Text Element')
86
+        ),
87
+        ElevatedButton(
88
+          onPressed: Provider.of<Editor>(context, listen: false).addTextboxElement, 
89
+          child: Text('Add Textbox Element')
90
+        ),
91
+        SizedBox(height: 24),
92
+
93
+
94
+        // ? Variable Element Section
95
+        Text('Insert Variable Element'),
96
+        SizedBox(height: 24),
97
+
98
+
99
+        ElevatedButton(
100
+          onPressed: Provider.of<Editor>(context, listen: false).addProductNameElement, 
101
+          child: Text('Add Product Name Element')
102
+        ),
103
+        ElevatedButton(
104
+          onPressed: Provider.of<Editor>(context, listen: false).addVariantNameElement, 
105
+          child: Text('Add Variant Name Element')
106
+        ),
107
+        ElevatedButton(
108
+          onPressed: Provider.of<Editor>(context, listen: false).addProductionCodeElement, 
109
+          child: Text('Add Production Code Element')
110
+        ),
111
+        ElevatedButton(
112
+          onPressed: Provider.of<Editor>(context, listen: false).addProductionDateElement, 
113
+          child: Text('Add Production Date Element')
114
+        ),
115
+        ElevatedButton(
116
+          onPressed: Provider.of<Editor>(context, listen: false).addSerialNumberElement, 
117
+          child: Text('Add Serial Number Element')
118
+        ),
119
+      ]
120
+    );
121
+  }
122
+
123
+  
124
+}
125
+
126
+
127
+
128
+class ToolbarPropertiesWidget extends StatefulWidget {
129
+  final Editor editorProvider;
130
+
131
+  const ToolbarPropertiesWidget({super.key, required this.editorProvider});
132
+
133
+  @override
134
+  State<ToolbarPropertiesWidget> createState() => _ToolbarPropertiesWidgetState();
135
+}
136
+
137
+class _ToolbarPropertiesWidgetState extends State<ToolbarPropertiesWidget> {
17
   List<DropdownMenuItem<int>> fontSizeDropdownItems= [];
138
   List<DropdownMenuItem<int>> fontSizeDropdownItems= [];
18
 
139
 
19
   List<DropdownMenuItem<int>> qrSizeDropdownItems= [];
140
   List<DropdownMenuItem<int>> qrSizeDropdownItems= [];
20
 
141
 
142
+  // String? _valueOnStartEditing;
21
 
143
 
22
   @override
144
   @override
23
   void initState() {
145
   void initState() {
24
-    // TODO: implement initState
25
     super.initState();
146
     super.initState();
26
 
147
 
148
+    // _valueOnStartEditing = widget.editorProvider.selectedElm?.valueController.text; 
149
+
27
     populateFontSizeDropdownItems();
150
     populateFontSizeDropdownItems();
28
     populateQrSizeDropdownItems();
151
     populateQrSizeDropdownItems();
152
+
153
+    log('[TOOLBAR PROPERTIES EXECUTED]');
154
+
29
   }
155
   }
30
 
156
 
31
   //functions
157
   //functions
53
     });
179
     });
54
   }
180
   }
55
 
181
 
182
+
56
   @override
183
   @override
57
   Widget build(BuildContext context) {
184
   Widget build(BuildContext context) {
58
-    final editorProvider = Provider.of<Editor>(context);
59
-
60
-    return Container(
61
-      padding: EdgeInsets.symmetric(horizontal: 16),
62
-      color: Colors.white,
63
-      width: MediaQuery.of(context).size.width,
64
-      height: 150,
65
-      child: ListView(
66
-        children: Provider.of<Editor>(context).insertElementMode ? insertElementSection() : elementPropertiesSection(editorProvider),
67
-      ),
185
+    return ListView(
186
+      children: elementPropertiesSection(widget.editorProvider),
68
     );
187
     );
69
   }
188
   }
70
 
189
 
71
-  List<Widget> insertElementSection() {
72
-    return [
73
-      Text('Insert Element'),
74
-      SizedBox(height: 20),
75
-      ElevatedButton(
76
-        onPressed: Provider.of<Editor>(context, listen: false).addTextElement, 
77
-        child: Text('Add Text Element')
78
-      ),
79
-      ElevatedButton(
80
-        onPressed: Provider.of<Editor>(context, listen: false).addTextboxElement, 
81
-        child: Text('Add Textbox Element')
82
-      ),
83
-      SizedBox(height: 24),
84
-
85
-
86
-      // ? Variable Element Section
87
-      Text('Insert Variable Element'),
88
-      SizedBox(height: 24),
89
-
90
-
91
-      ElevatedButton(
92
-        onPressed: Provider.of<Editor>(context, listen: false).addProductNameElement, 
93
-        child: Text('Add Product Name Element')
94
-      ),
95
-      ElevatedButton(
96
-        onPressed: Provider.of<Editor>(context, listen: false).addVariantNameElement, 
97
-        child: Text('Add Variant Name Element')
98
-      ),
99
-      ElevatedButton(
100
-        onPressed: Provider.of<Editor>(context, listen: false).addProductionCodeElement, 
101
-        child: Text('Add Production Code Element')
102
-      ),
103
-      ElevatedButton(
104
-        onPressed: Provider.of<Editor>(context, listen: false).addProductionDateElement, 
105
-        child: Text('Add Production Date Element')
106
-      ),
107
-      ElevatedButton(
108
-        onPressed: Provider.of<Editor>(context, listen: false).addSerialNumberElement, 
109
-        child: Text('Add Serial Number Element')
110
-      ),
111
-    ];
112
-  }
113
-
114
   List<Widget> elementPropertiesSection(Editor editorProvider) {
190
   List<Widget> elementPropertiesSection(Editor editorProvider) {
115
     final element = Provider.of<Editor>(context).selectedElm;
191
     final element = Provider.of<Editor>(context).selectedElm;
116
 
192
 
130
   }
206
   }
131
 
207
 
132
 
208
 
133
-  List<Widget> _variablePropertiesSection(Editor editorProvider, ElementProperty? element) {
209
+  List<Widget> _variablePropertiesSection(Editor editorProvider, ElementState? element) {
134
     return [
210
     return [
135
       Container(
211
       Container(
136
         margin: EdgeInsets.only(bottom: 16),
212
         margin: EdgeInsets.only(bottom: 16),
151
     ];
227
     ];
152
   }
228
   }
153
 
229
 
154
-  List<Widget> _commonPropertiesSection(Editor editorProvider, ElementProperty? element) {
230
+  List<Widget> _commonPropertiesSection(Editor editorProvider, ElementState? element) {
231
+
155
     return [
232
     return [
156
       // ? Value Editor
233
       // ? Value Editor
157
       if([ElementType.text, ElementType.textbox].contains(editorProvider.selectedElm!.type)) TextField(
234
       if([ElementType.text, ElementType.textbox].contains(editorProvider.selectedElm!.type)) TextField(
158
         readOnly: element!.isLocked, 
235
         readOnly: element!.isLocked, 
159
         controller: element!.valueController,
236
         controller: element!.valueController,
160
         onTap: editorProvider.enableEdit,
237
         onTap: editorProvider.enableEdit,
161
-        onEditingComplete: () {
162
-          FocusManager.instance.primaryFocus?.unfocus();
238
+        // onEditingComplete: () {
239
+        //   FocusManager.instance.primaryFocus?.unfocus();
163
 
240
 
164
-          editorProvider.disableEdit();
241
+        //   editorProvider.disableEdit();
165
 
242
 
166
-          print('kepenjet');
167
-        },
168
-        onChanged: (value) {
169
-          setState(() {
170
-            
243
+        //   print('kepenjet');
244
+        // },
245
+        onChanged: (newText) {
246
+          Debouncer.run(() {
247
+            log('[SAVING TO HISTORY]');
248
+            Provider.of<Editor>(context, listen: false).editText(element.valueController);
249
+            element.valueController.text = widget.editorProvider.valueOnStartEditing ?? '';
250
+
251
+            widget.editorProvider.setValueOnStartEditing(newText);
171
           });
252
           });
253
+
254
+
255
+          setState(() {});
172
         },
256
         },
173
       ),
257
       ),
174
       
258
       
189
   }
273
   }
190
 
274
 
191
 
275
 
192
-  Widget _buildFontResizerWidget(ElementProperty? element) {
276
+  Widget _buildFontResizerWidget(ElementState? element) {
193
     return Row(
277
     return Row(
194
       children: [
278
       children: [
195
         DropdownButton<int>(
279
         DropdownButton<int>(
212
     );
296
     );
213
   }
297
   }
214
 
298
 
215
-  Widget _buildQrResizerWidget(ElementProperty? element) {
299
+  Widget _buildQrResizerWidget(ElementState? element) {
216
     return  Row(
300
     return  Row(
217
       children: [
301
       children: [
218
         DropdownButton<int>(
302
         DropdownButton<int>(

+ 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