Browse Source

feat: history-undo-redo (refactored-stable)

Raihan Rizal 4 months ago
parent
commit
56f0bd690d
7 changed files with 859 additions and 568 deletions
  1. 31 2
      lib/canvas_setup_page.dart
  2. 2 2
      lib/history.dart
  3. 2 2
      lib/main.dart
  4. 313 148
      lib/providers/editor.dart
  5. 32 0
      lib/utils/debouncer.dart
  6. 338 303
      lib/widgets/elements.dart
  7. 141 111
      lib/widgets/toolbar.dart

+ 31 - 2
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
               ),

+ 2 - 2
lib/history.dart

5
 
5
 
6
 class CanvasHistory {
6
 class CanvasHistory {
7
   final CanvasHistoryModifyType type;
7
   final CanvasHistoryModifyType type;
8
-  List<ElementProperty> elementPropeties;
8
+  List<ElementState> elementPropeties;
9
 
9
 
10
   CanvasHistory(this.type, this.elementPropeties);
10
   CanvasHistory(this.type, this.elementPropeties);
11
 
11
 
12
-  List<ElementProperty> get getState => elementPropeties;
12
+  List<ElementState> get getState => elementPropeties;
13
 }
13
 }

+ 2 - 2
lib/main.dart

107
           actions: [
107
           actions: [
108
 
108
 
109
             IconButton(
109
             IconButton(
110
-              onPressed: Provider.of<Editor>(context, listen: false).undoStack.isEmpty ? null : () {
110
+              onPressed: Provider.of<Editor>(context, listen: false).stateStack.length <= 1 ? null : () {
111
                 Provider.of<Editor>(context, listen: false).undo();
111
                 Provider.of<Editor>(context, listen: false).undo();
112
               },
112
               },
113
               icon: Icon(Icons.undo)
113
               icon: Icon(Icons.undo)
165
                             width: Provider.of<Editor>(context).canvasProperty.width,
165
                             width: Provider.of<Editor>(context).canvasProperty.width,
166
                             child: Stack(
166
                             child: Stack(
167
                               children: [
167
                               children: [
168
-                                for (ElementProperty elementProperty in Provider.of<Editor>(context).elementProperties) ... [
168
+                                for (ElementState elementProperty in Provider.of<Editor>(context).currentElementsState) ... [
169
                                   ElementWidget(
169
                                   ElementWidget(
170
                                     elementProperty: elementProperty,
170
                                     elementProperty: elementProperty,
171
                                     canvasProperty: Provider.of<Editor>(context).canvasProperty,
171
                                     canvasProperty: Provider.of<Editor>(context).canvasProperty,

+ 313 - 148
lib/providers/editor.dart

23
 
23
 
24
   final textboxResizerDeferredPointerHandlerLink = DeferredPointerHandlerLink();
24
   final textboxResizerDeferredPointerHandlerLink = DeferredPointerHandlerLink();
25
 
25
 
26
+  String? valueOnStartEditing;
26
 
27
 
27
 
28
 
28
   // ? Canvas State
29
   // ? Canvas State
55
 
56
 
56
 
57
 
57
 
58
 
58
-  List<ElementProperty> elementProperties = [
59
-    ElementProperty(
60
-      id: uuid.v4(), 
61
-      valueController: TextEditingController(text: '{{QRCODE}}'), 
62
-      type: ElementType.qr, 
63
-      position: ElementPosition(top: 0, left: 0), 
64
-      width: 80, 
65
-      quarterTurns: 0, 
66
-      elementKey: GlobalKey(),
67
-      qrScale: 3
68
-    )
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
+    ]
69
   ];
87
   ];
70
 
88
 
89
+  // current canvas state
90
+  List<ElementState> get currentElementsState => stateStack.last;
91
+
71
 
92
 
72
 
93
 
73
 
94
 
74
   // ? History stack
95
   // ? History stack
75
-  List<CanvasHistory> undoStack = [];
76
-  List<List<ElementProperty>> redoStack = [];
96
+  // List<CanvasHistory> undoStack = [];
97
+  List<List<ElementState>> redoStack = [];
98
+
77
 
99
 
100
+  void setNewElementsState(List<ElementState> newElementsState) {
101
+    _setNewStateTextEdit(newElementsState);
78
 
102
 
79
-  void addUndoEntry(CanvasHistoryModifyType canvasHistoryType, List<ElementProperty> elementProperties) {
80
-    undoStack.add(CanvasHistory(canvasHistoryType, _cloneCurrentState(elementProperties)));
103
+    stateStack.add(newElementsState);
81
 
104
 
82
     redoStack.clear();
105
     redoStack.clear();
83
 
106
 
85
   }
108
   }
86
 
109
 
87
   void undo() {
110
   void undo() {
88
-    addRedoEntry(elementProperties);
111
+    addRedoEntry(stateStack.last);
89
 
112
 
90
     // apply to current state
113
     // apply to current state
91
-    if (undoStack.last.type == CanvasHistoryModifyType.textEdit && undoStack.last.getState != elementProperties) {
92
-      undoStack.removeLast();
93
-    }
114
+    // if (undoStack.last.type == CanvasHistoryModifyType.textEdit && undoStack.last.getState != elementProperties) {
115
+    //   undoStack.removeLast();
116
+    // }
94
 
117
 
95
-    elementProperties = undoStack.last.getState;
118
+    // elementProperties = undoStack.last.getState;
119
+
120
+    stateStack.removeLast();
96
 
121
 
97
     // unselect element
122
     // unselect element
98
-    if (elementProperties.firstWhereOrNull((e) => e.id == selectedElmId) == null) {
123
+    if (currentElementsState.firstWhereOrNull((e) => e.id == selectedElmId) == null) {
99
       unSelectElm();
124
       unSelectElm();
100
     }
125
     }
101
 
126
 
102
-    undoStack.removeLast();
103
-
104
     notifyListeners();
127
     notifyListeners();
105
   }
128
   }
106
 
129
 
107
 
130
 
108
-  void addRedoEntry(List<ElementProperty> elementProperties) {
109
-    redoStack.add(_cloneCurrentState(elementProperties));
131
+  void addRedoEntry(List<ElementState> elementProperties) {
132
+    redoStack.add(_cloneElementsState(elementProperties));
110
 
133
 
111
     notifyListeners();
134
     notifyListeners();
112
   }
135
   }
113
 
136
 
114
   void redo() {
137
   void redo() {
115
-    undoStack.add(CanvasHistory(CanvasHistoryModifyType.redo, _cloneCurrentState(elementProperties)));
138
+    // undoStack.add(CanvasHistory(CanvasHistoryModifyType.redo, _cloneCurrentState(elementProperties)));
139
+    stateStack.add(_cloneElementsState(redoStack.last));
116
      
140
      
117
     // apply to current state
141
     // apply to current state
118
-    elementProperties = redoStack.last;
142
+    // elementProperties = redoStack.last;
119
 
143
 
120
     redoStack.removeLast();
144
     redoStack.removeLast();
121
 
145
 
122
     notifyListeners();
146
     notifyListeners();
123
   }
147
   }
124
 
148
 
125
-  List<ElementProperty> _cloneCurrentState(List<ElementProperty> elementProperties) {
126
-    List<ElementProperty> clonedElementProperties = elementProperties.map((elementProperty) {
127
-      return ElementProperty(
149
+  List<ElementState> _cloneElementsState(List<ElementState> elementProperties) {
150
+    List<ElementState> clonedElementProperties = elementProperties.map((elementProperty) {
151
+      return ElementState(
128
         id: elementProperty.id,
152
         id: elementProperty.id,
129
         valueController: TextEditingController(text: elementProperty.valueController.text),
153
         valueController: TextEditingController(text: elementProperty.valueController.text),
130
         type: elementProperty.type,
154
         type: elementProperty.type,
150
     canvasProperty.height = height;
174
     canvasProperty.height = height;
151
     canvasProperty.width = width;
175
     canvasProperty.width = width;
152
     
176
     
153
-    undoStack.clear();
177
+    // undoStack.clear();
178
+    stateStack = [currentElementsState];
154
     redoStack.clear();
179
     redoStack.clear();
155
 
180
 
156
     _adjustOutOfBoundElement();
181
     _adjustOutOfBoundElement();
161
   }
186
   }
162
 
187
 
163
   void _adjustOutOfBoundElement() {
188
   void _adjustOutOfBoundElement() {
164
-    for (var element in elementProperties) {
189
+    for (var element in currentElementsState) {
165
       bool isOutOfBoundFromTop = (element.position.top + 10) > canvasProperty.height;
190
       bool isOutOfBoundFromTop = (element.position.top + 10) > canvasProperty.height;
166
       bool isOutOfBoundFromLeft = (element.position.left + 10) > canvasProperty.width;
191
       bool isOutOfBoundFromLeft = (element.position.left + 10) > canvasProperty.width;
167
 
192
 
182
 
207
 
183
   }
208
   }
184
 
209
 
185
-  void populateElement(List<ElementProperty> elementProperties) {
186
-    this.elementProperties.addAll(elementProperties);
187
-    // _reservedIdUntil = this.elementProperties.length - 1;
188
-    notifyListeners();
189
-  }
190
 
210
 
211
+
212
+
213
+
214
+
215
+  // ? Primitive Element
191
   void addTextElement(){
216
   void addTextElement(){
192
     String id = uuid.v4();
217
     String id = uuid.v4();
193
 
218
 
194
-    ElementProperty element = ElementProperty(
219
+    ElementState newElement = ElementState(
195
       id: id, 
220
       id: id, 
196
       valueController: TextEditingController(text: 'Double tap to edit text'), 
221
       valueController: TextEditingController(text: 'Double tap to edit text'), 
197
       type: ElementType.text, 
222
       type: ElementType.text, 
203
     );
228
     );
204
 
229
 
205
 
230
 
206
-    // Save History
207
-    addUndoEntry(CanvasHistoryModifyType.add, elementProperties);
231
+    // Set State
232
+    // List<ElementState> newElementsState = [..._cloneElementsState(currentElementsState), newElement];
233
+    // setNewElementsState(newElementsState);
234
+    _setAddNewElementState(newElement);
208
 
235
 
209
 
236
 
210
-    elementProperties.add(element);
211
-    // _reservedIdUntil = elementProperties.length - 1;r
212
     notifyListeners();
237
     notifyListeners();
213
-
214
     selectElmById(id);
238
     selectElmById(id);
215
   } 
239
   } 
216
 
240
 
217
   void addTextboxElement() {
241
   void addTextboxElement() {
218
     String id = uuid.v4();
242
     String id = uuid.v4();
219
 
243
 
220
-    ElementProperty element = ElementProperty(
244
+    ElementState newElement = ElementState(
221
       id: id, 
245
       id: id, 
222
       valueController: TextEditingController(text: 'Double tap to edit text'), 
246
       valueController: TextEditingController(text: 'Double tap to edit text'), 
223
       type: ElementType.textbox, 
247
       type: ElementType.textbox, 
227
       elementKey: GlobalKey()
251
       elementKey: GlobalKey()
228
     );
252
     );
229
 
253
 
230
-    // Save History
231
-    addUndoEntry(CanvasHistoryModifyType.add, elementProperties);
232
 
254
 
233
-    elementProperties.add(element);
234
-    notifyListeners();
255
+    // Set State
256
+    _setAddNewElementState(newElement);
235
 
257
 
258
+    notifyListeners();
236
     selectElmById(id);
259
     selectElmById(id);
237
   }
260
   }
238
 
261
 
239
-  // ? Variable Element
240
 
262
 
263
+
264
+
265
+
266
+  // ? Variable Element
241
   void addProductNameElement() {
267
   void addProductNameElement() {
242
     String id = uuid.v4();
268
     String id = uuid.v4();
243
 
269
 
244
-    ElementProperty element = ElementProperty(
270
+    ElementState newElement = ElementState(
245
       id: id, 
271
       id: id, 
246
       valueController: TextEditingController(text: '{{PRODUCTNAME}}'), 
272
       valueController: TextEditingController(text: '{{PRODUCTNAME}}'), 
247
       type: ElementType.textbox,
273
       type: ElementType.textbox,
252
       elementKey: GlobalKey()
278
       elementKey: GlobalKey()
253
     );
279
     );
254
 
280
 
255
-    // Save History
256
-    addUndoEntry(CanvasHistoryModifyType.add, elementProperties);
281
+    // Set State
282
+    _setAddNewElementState(newElement);
257
 
283
 
258
-    elementProperties.add(element);
259
     notifyListeners();
284
     notifyListeners();
260
-
261
     selectElmById(id);
285
     selectElmById(id);
262
   }
286
   }
263
 
287
 
264
   void addVariantNameElement() {
288
   void addVariantNameElement() {
265
     String id = uuid.v4();
289
     String id = uuid.v4();
266
 
290
 
267
-    ElementProperty element = ElementProperty(
291
+    ElementState newElement = ElementState(
268
       id: id, 
292
       id: id, 
269
       valueController: TextEditingController(text: '{{VARIANTNAME}}'), 
293
       valueController: TextEditingController(text: '{{VARIANTNAME}}'), 
270
       type: ElementType.textbox,
294
       type: ElementType.textbox,
275
       elementKey: GlobalKey()
299
       elementKey: GlobalKey()
276
     );
300
     );
277
 
301
 
278
-    // Save History
279
-    addUndoEntry(CanvasHistoryModifyType.add, elementProperties);
302
+    // Set State
303
+    _setAddNewElementState(newElement);
280
 
304
 
281
-    elementProperties.add(element);
282
     notifyListeners();
305
     notifyListeners();
283
-
284
     selectElmById(id);
306
     selectElmById(id);
285
   }
307
   }
286
 
308
 
287
   void addProductionCodeElement() {
309
   void addProductionCodeElement() {
288
     String id = uuid.v4();
310
     String id = uuid.v4();
289
 
311
 
290
-    ElementProperty element = ElementProperty(
312
+    ElementState newElement = ElementState(
291
       id: id, 
313
       id: id, 
292
       valueController: TextEditingController(text: '{{PRODUCTIONCODE}}'), 
314
       valueController: TextEditingController(text: '{{PRODUCTIONCODE}}'), 
293
       type: ElementType.textbox,
315
       type: ElementType.textbox,
298
       elementKey: GlobalKey()
320
       elementKey: GlobalKey()
299
     );
321
     );
300
 
322
 
301
-    // Save History
302
-    addUndoEntry(CanvasHistoryModifyType.add, elementProperties);
323
+    // Set State
324
+    _setAddNewElementState(newElement);
303
 
325
 
304
-    elementProperties.add(element);
305
     notifyListeners();
326
     notifyListeners();
306
-
307
     selectElmById(id);
327
     selectElmById(id);
308
   }
328
   }
309
 
329
 
310
   void addProductionDateElement() {
330
   void addProductionDateElement() {
311
     String id = uuid.v4();
331
     String id = uuid.v4();
312
 
332
 
313
-    ElementProperty element = ElementProperty(
333
+    ElementState newElement = ElementState(
314
       id: id, 
334
       id: id, 
315
       valueController: TextEditingController(text: '{{PRODUCTIONDATE}}'), 
335
       valueController: TextEditingController(text: '{{PRODUCTIONDATE}}'), 
316
       type: ElementType.text,
336
       type: ElementType.text,
322
     );
342
     );
323
 
343
 
324
     // Save History
344
     // Save History
325
-    addUndoEntry(CanvasHistoryModifyType.add, elementProperties);
345
+    _setAddNewElementState(newElement);
326
 
346
 
327
-    elementProperties.add(element);
328
     notifyListeners();
347
     notifyListeners();
329
-
330
     selectElmById(id);
348
     selectElmById(id);
331
   }
349
   }
332
 
350
 
333
   void addSerialNumberElement() {
351
   void addSerialNumberElement() {
334
     String id = uuid.v4();
352
     String id = uuid.v4();
335
 
353
 
336
-    ElementProperty element = ElementProperty(
354
+    ElementState newElement = ElementState(
337
       id: id, 
355
       id: id, 
338
       valueController: TextEditingController(text: '{{SERIALNUMBER}}'), 
356
       valueController: TextEditingController(text: '{{SERIALNUMBER}}'), 
339
       type: ElementType.text,
357
       type: ElementType.text,
344
       elementKey: GlobalKey()
362
       elementKey: GlobalKey()
345
     );
363
     );
346
 
364
 
347
-    // Save History
348
-    addUndoEntry(CanvasHistoryModifyType.add, elementProperties);
365
+    // Set State
366
+    _setAddNewElementState(newElement);
349
 
367
 
350
-    elementProperties.add(element);
351
     notifyListeners();
368
     notifyListeners();
352
-
353
     selectElmById(id);
369
     selectElmById(id);
354
   }
370
   }
355
-  
371
+
356
 
372
 
357
 
373
 
358
 
374
 
359
 
375
 
360
   void updateElmPosition(Offset offset) {
376
   void updateElmPosition(Offset offset) {
361
-    ElementProperty? element = selectedElm;
377
+    ElementState? element = selectedElm;
362
 
378
 
363
     if (element == null) return;
379
     if (element == null) return;
364
 
380
 
369
     notifyListeners();
385
     notifyListeners();
370
   }
386
   }
371
 
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
+
372
   void updateElmWitdh(double width) {
422
   void updateElmWitdh(double width) {
373
-    ElementProperty? element = selectedElm;
423
+    ElementState? element = selectedElm;
374
 
424
 
375
     if (element == null) return;
425
     if (element == null) return;
376
 
426
 
379
     notifyListeners();
429
     notifyListeners();
380
   }
430
   }
381
 
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
+
382
   void toggleLockElement() {
452
   void toggleLockElement() {
383
     if (selectedElm == null) return;
453
     if (selectedElm == null) return;
384
 
454
 
385
-    // ? Save History
386
-    addUndoEntry(CanvasHistoryModifyType.lock, elementProperties);
455
+    List<ElementState> newElementsState = _cloneElementsState(currentElementsState);
456
+    var selectedNewElement = newElementsState.firstWhere((e) => e.id == selectedElm!.id);
457
+    
458
+    selectedNewElement.isLocked = !selectedNewElement.isLocked;
387
 
459
 
388
-    selectedElm!.isLocked = !selectedElm!.isLocked;
460
+    setNewElementsState(newElementsState);
461
+
462
+    // selectedElm!.isLocked = !selectedElm!.isLocked;
389
     notifyListeners();
463
     notifyListeners();
390
   }
464
   }
391
 
465
 
466
+  bool shouldIgnoreTouch(String elementId) {
467
+    if (elementId == selectedElmId) return false;
468
+
469
+    if (selectedElmId == null) return false;
470
+
471
+    return true;
472
+  }
392
 
473
 
393
   void selectElmById(String id) {
474
   void selectElmById(String id) {
394
     selectedElmId = id;
475
     selectedElmId = id;
476
+    valueOnStartEditing = currentElementsState.firstWhere((e) => e.id == selectedElmId).valueController.text;
395
     notifyListeners();
477
     notifyListeners();
396
   }
478
   }
397
 
479
 
415
   void unSelectElm() {
497
   void unSelectElm() {
416
     selectedElmId = null;
498
     selectedElmId = null;
417
     isEditing = false;
499
     isEditing = false;
500
+    valueOnStartEditing = null;
418
     notifyListeners();
501
     notifyListeners();
419
   }
502
   }
420
 
503
 
424
 
507
 
425
   // ? Getters
508
   // ? Getters
426
   String get selectedElmType {
509
   String get selectedElmType {
427
-    if (elementProperties.isNotEmpty && selectedElmId != null) {
428
-      final selectedElm = elementProperties.firstWhere((element) => element.id == selectedElmId);
510
+    if (currentElementsState.isNotEmpty && selectedElmId != null) {
511
+      final selectedElm = currentElementsState.firstWhere((element) => element.id == selectedElmId);
429
       return elementGetter(selectedElm.type);
512
       return elementGetter(selectedElm.type);
430
     }
513
     }
431
 
514
 
432
     return '';
515
     return '';
433
   }
516
   }
434
 
517
 
435
-  ElementProperty? get selectedElm {
436
-    if (elementProperties.isNotEmpty && selectedElmId != null) {
437
-      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);
438
     }
521
     }
439
 
522
 
440
     return null;
523
     return null;
441
   }
524
   }
442
 
525
 
443
   GlobalKey? get selectedElmKey {
526
   GlobalKey? get selectedElmKey {
444
-    if (elementProperties.isNotEmpty && selectedElmId != null) {
445
-      return elementProperties.firstWhere((element) => element.id == selectedElmId).elementKey;
527
+    if (currentElementsState.isNotEmpty && selectedElmId != null) {
528
+      return currentElementsState.firstWhere((element) => element.id == selectedElmId).elementKey;
446
     }
529
     }
447
 
530
 
448
     return null;
531
     return null;
513
 
596
 
514
   /// Can only rotate [ElementType.text, ElementType.textBox]
597
   /// Can only rotate [ElementType.text, ElementType.textBox]
515
   void rotate() {
598
   void rotate() {
516
-    ElementProperty? element = selectedElm;
517
-
518
-    if (element == null) return;
599
+    if (selectedElm == null) return;
519
 
600
 
520
-    if (element.isLocked) {
601
+    if (selectedElm!.isLocked) {
521
       _showLockedToast('Cant rotate locked element');
602
       _showLockedToast('Cant rotate locked element');
522
       return;
603
       return;
523
     }
604
     }
524
     
605
     
525
-    if (![ElementType.text, ElementType.textbox].contains(element.type)) return;
606
+    if (![ElementType.text, ElementType.textbox].contains(selectedElm!.type)) return;
526
     
607
     
527
-    // ? Save history
528
-    addUndoEntry(CanvasHistoryModifyType.rotate, elementProperties);
608
+    List<ElementState> newElementsState = _cloneElementsState(currentElementsState);
609
+    var newElement = newElementsState.firstWhere((e) => e.id == selectedElmId);
610
+
611
+
612
+    if (newElement.type == ElementType.text) _rotateText(newElement);
613
+
614
+    if (newElement.type == ElementType.textbox) _rotateTextBox(newElement);
529
 
615
 
530
-    if (element.type == ElementType.text) _rotateText(element);
531
 
616
 
532
-    if (element.type == ElementType.textbox) _rotateTextBox(element);
617
+    setNewElementsState(newElementsState);
533
 
618
 
534
 
619
 
535
     // Adjust Size
620
     // Adjust Size
544
 
629
 
545
   }
630
   }
546
 
631
 
547
-  void _rotateText(ElementProperty element) {
548
-    if (element.quarterTurns < 3) {
549
-      element.quarterTurns += 1;
550
-    } else {
551
-      element.quarterTurns = 0;
552
-    }
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);
553
   }
645
   }
554
 
646
 
555
-  void _rotateTextBox(ElementProperty element) {
556
-    if (element.quarterTurns == 0) {
557
-      element.quarterTurns = 3;
558
-    } else {
559
-      element.quarterTurns = 0;
560
-    }
647
+  void setValueOnStartEditing(String text) {
648
+    valueOnStartEditing = text;
561
   }
649
   }
562
 
650
 
651
+
563
   // FontSize Handler
652
   // FontSize Handler
564
   void changeFontSize(int? fontSize) {
653
   void changeFontSize(int? fontSize) {
565
     if (fontSize == null) return;
654
     if (fontSize == null) return;
571
       return;
660
       return;
572
     }
661
     }
573
 
662
 
574
-    // ? Save History
575
-    addUndoEntry(CanvasHistoryModifyType.resize, elementProperties);
663
+    List<ElementState> newElementsState = _cloneElementsState(currentElementsState);
664
+    var newElement = newElementsState.firstWhere((e) => e.id == selectedElmId);
665
+
666
+    newElement.fontScale = fontSize;
667
+
668
+    setNewElementsState(newElementsState);
576
 
669
 
577
-    selectedElm!.fontScale = fontSize;
578
     notifyListeners();
670
     notifyListeners();
579
   }
671
   }
580
   
672
   
589
     }
681
     }
590
     // check if value is allowed for resize
682
     // check if value is allowed for resize
591
     if (CanvasStyle.fontSizeMap.containsKey(incrementTo)) {
683
     if (CanvasStyle.fontSizeMap.containsKey(incrementTo)) {
592
-      // ? Save History
593
-      addUndoEntry(CanvasHistoryModifyType.resize, elementProperties);
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;
594
 
692
 
595
-      selectedElm!.fontScale = incrementTo;
693
+      setNewElementsState(newElementsState);
596
       print('kepenjet increase');
694
       print('kepenjet increase');
597
     } else {
695
     } else {
598
       print('cant increment');
696
       print('cant increment');
612
     }
710
     }
613
     // check if value is allowed for resize
711
     // check if value is allowed for resize
614
     if (CanvasStyle.fontSizeMap.containsKey(decrementTo)) {
712
     if (CanvasStyle.fontSizeMap.containsKey(decrementTo)) {
615
-      // ? Save History
616
-      addUndoEntry(CanvasHistoryModifyType.resize, elementProperties);
713
+      List<ElementState> newElementsState = _cloneElementsState(currentElementsState);
714
+      var newElement = newElementsState.firstWhere((e) => e.id == selectedElmId);
617
 
715
 
618
-      selectedElm!.fontScale = decrementTo;
716
+      newElement.fontScale = decrementTo;
717
+
718
+      setNewElementsState(newElementsState);
619
     } else {
719
     } else {
620
       print('cant decrement');
720
       print('cant decrement');
621
     }
721
     }
622
-
623
-    notifyListeners();
624
   }
722
   }
625
 
723
 
626
   // Qr Size Handler
724
   // Qr Size Handler
634
       return;
732
       return;
635
     }
733
     }
636
 
734
 
637
-    selectedElm!.qrScale = fontSize;
638
-    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);
639
   }
742
   }
640
   
743
   
641
   void incrementQrSize() {
744
   void incrementQrSize() {
651
     }
754
     }
652
     // check if value is allowed for resize
755
     // check if value is allowed for resize
653
     if (CanvasStyle.qrSizeMap.containsKey(incrementTo)) {
756
     if (CanvasStyle.qrSizeMap.containsKey(incrementTo)) {
654
-      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);
655
     } else {
764
     } else {
656
       print('cant increment');
765
       print('cant increment');
657
     }
766
     }
658
 
767
 
659
-    notifyListeners();
660
   }
768
   }
661
 
769
 
662
   void decrementQrSize() {
770
   void decrementQrSize() {
674
 
782
 
675
     // check if value is allowed for resize
783
     // check if value is allowed for resize
676
     if (CanvasStyle.qrSizeMap.containsKey(decrementTo)) {
784
     if (CanvasStyle.qrSizeMap.containsKey(decrementTo)) {
677
-      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);
678
     } else {
792
     } else {
679
       print('cant decrement');
793
       print('cant decrement');
680
     }
794
     }
713
 
827
 
714
     if (!shouldDelete) return;
828
     if (!shouldDelete) return;
715
 
829
 
716
-    // ? Save History
717
-    addUndoEntry(CanvasHistoryModifyType.remove, elementProperties);
830
+    // // ? Save History
831
+    // setNewElementsState(CanvasHistoryModifyType.remove, elementProperties);
718
 
832
 
719
-    elementProperties.removeWhere((e) => e.id == selectedElm!.id);
720
-    unSelectElm();
833
+    // elementProperties.removeWhere((e) => e.id == selectedElm!.id);
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);
721
 
842
 
843
+    setNewElementsState(newElementsState);
844
+    
845
+    unSelectElm();
722
     notifyListeners();
846
     notifyListeners();
723
   }
847
   }
724
 
848
 
730
   //   }
854
   //   }
731
   // }
855
   // }
732
 
856
 
733
-  // ? helper method
734
-  void _showLockedToast(String titleText) {
735
-    toastification.show(
736
-      title: Text(titleText),
737
-      description: Text('unlock to change element property'),
738
-      closeButtonShowType: CloseButtonShowType.none,
739
-      style: ToastificationStyle.minimal,
740
-      type: ToastificationType.warning,
741
-      autoCloseDuration: const Duration(seconds: 3),
742
-      alignment: Alignment.bottomCenter,
743
-      dragToClose: true
744
-    );
745
-  }
746
-
747
-
748
-
749
 
857
 
750
   // ? Template to JSON
858
   // ? Template to JSON
751
   String buildJSON() {
859
   String buildJSON() {
762
       }
870
       }
763
     };
871
     };
764
 
872
 
765
-    for (var element in elementProperties) {
873
+    for (var element in currentElementsState) {
766
       var elementMap = {
874
       var elementMap = {
767
-        'id': uuid.v4(),
875
+        'id': element.id,
768
         'content': element.valueController.text,
876
         'content': element.valueController.text,
769
         'height': element.elementKey.currentContext?.size?.height.round() ?? 0,
877
         'height': element.elementKey.currentContext?.size?.height.round() ?? 0,
770
         'width': element.elementKey.currentContext?.size?.width.round() ?? 0,
878
         'width': element.elementKey.currentContext?.size?.width.round() ?? 0,
793
   }
901
   }
794
 
902
 
795
 
903
 
796
-  String _getElementTypeResult(ElementProperty element) {
904
+  String _getElementTypeResult(ElementState element) {
797
     switch (element.type) {
905
     switch (element.type) {
798
       case ElementType.text:
906
       case ElementType.text:
799
         return 'text';
907
         return 'text';
819
     }
927
     }
820
   }
928
   }
821
 
929
 
822
-  int _getElementSizeResult(ElementProperty element) {
930
+  int _getElementSizeResult(ElementState element) {
823
     if (element.type == ElementType.qr) {
931
     if (element.type == ElementType.qr) {
824
       return element.qrScale ?? 1;
932
       return element.qrScale ?? 1;
825
     }
933
     }
826
 
934
 
827
     return element.fontScale;
935
     return element.fontScale;
828
   }
936
   }
829
-}
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 }

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

+ 338 - 303
lib/widgets/elements.dart

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

+ 141 - 111
lib/widgets/toolbar.dart

1
+import 'dart:developer';
2
+
1
 import 'package:easy_debounce/easy_debounce.dart';
3
 import 'package:easy_debounce/easy_debounce.dart';
2
 import 'package:flutter/material.dart';
4
 import 'package:flutter/material.dart';
3
 import 'package:flutter_canvas_editor/history.dart';
5
 import 'package:flutter_canvas_editor/history.dart';
4
 import 'package:flutter_canvas_editor/providers/editor.dart';
6
 import 'package:flutter_canvas_editor/providers/editor.dart';
5
 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';
6
 import 'package:flutter_canvas_editor/widgets/elements.dart';
9
 import 'package:flutter_canvas_editor/widgets/elements.dart';
7
 import 'package:provider/provider.dart';
10
 import 'package:provider/provider.dart';
8
 
11
 
16
 }
19
 }
17
 
20
 
18
 class _ToolbarWidgetState extends State<ToolbarWidget> {
21
 class _ToolbarWidgetState extends State<ToolbarWidget> {
19
-  List<DropdownMenuItem<int>> fontSizeDropdownItems= [];
20
-
21
-  List<DropdownMenuItem<int>> qrSizeDropdownItems= [];
22
+  
22
 
23
 
23
   FocusNode? _focus = FocusNode();
24
   FocusNode? _focus = FocusNode();
24
 
25
 
25
   // String? _lastSavedText;
26
   // String? _lastSavedText;
26
 
27
 
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> {
138
+  List<DropdownMenuItem<int>> fontSizeDropdownItems= [];
139
+
140
+  List<DropdownMenuItem<int>> qrSizeDropdownItems= [];
141
+
142
+  // String? _valueOnStartEditing;
143
+
28
   @override
144
   @override
29
   void initState() {
145
   void initState() {
30
-    // TODO: implement initState
31
     super.initState();
146
     super.initState();
32
 
147
 
148
+    // _valueOnStartEditing = widget.editorProvider.selectedElm?.valueController.text; 
149
+
33
     populateFontSizeDropdownItems();
150
     populateFontSizeDropdownItems();
34
     populateQrSizeDropdownItems();
151
     populateQrSizeDropdownItems();
35
 
152
 
36
-    WidgetsBinding.instance.addPostFrameCallback((_) {
37
-      _addFocusNodeListener();
38
-    });
153
+    log('[TOOLBAR PROPERTIES EXECUTED]');
154
+
39
   }
155
   }
40
 
156
 
41
   //functions
157
   //functions
63
     });
179
     });
64
   }
180
   }
65
 
181
 
66
-  void _addFocusNodeListener() {
67
-    if (Provider.of<Editor>(context, listen: false).selectedElmKey != null && [ElementType.text, ElementType.textbox].contains(Provider.of<Editor>(context, listen: false).selectedElm!.type)) {
68
-      _focus!.addListener(_onTextFieldFocusChange);
69
-    }
70
-  }
71
-
72
-  void _onTextFieldFocusChange() {
73
-    print('toolbar onfocus change');
74
-
75
-    if (Provider.of<Editor>(context, listen: false).selectedElm?.lastSavedText != Provider.of<Editor>(context, listen: false).selectedElm?.valueController.text) {
76
-      Provider.of<Editor>(context, listen: false).addUndoEntry(CanvasHistoryModifyType.textEdit, Provider.of<Editor>(context, listen: false).elementProperties);
77
-      Provider.of<Editor>(context, listen: false).selectedElm?.lastSavedText = Provider.of<Editor>(context, listen: false).selectedElm?.valueController.text;
78
-    }
79
-  }
80
-
81
-  void _removeFocusNodeListener() {
82
-    if ([ElementType.text, ElementType.textbox].contains(Provider.of<Editor>(context).selectedElm!.type)) {
83
-      _focus!.removeListener(_onTextFieldFocusChange);
84
-    }
85
-  }
86
-
87
-
88
-  @override
89
-  void dispose() {
90
-    // TODO: implement dispose
91
-
92
-    _removeFocusNodeListener();
93
-    super.dispose();
94
-  }
95
-
96
 
182
 
97
   @override
183
   @override
98
   Widget build(BuildContext context) {
184
   Widget build(BuildContext context) {
99
-    final editorProvider = Provider.of<Editor>(context);
100
-
101
-    return Container(
102
-      padding: EdgeInsets.symmetric(horizontal: 16),
103
-      color: Colors.white,
104
-      width: MediaQuery.of(context).size.width,
105
-      height: 150,
106
-      child: ListView(
107
-        children: Provider.of<Editor>(context).insertElementMode ? insertElementSection() : elementPropertiesSection(editorProvider),
108
-      ),
185
+    return ListView(
186
+      children: elementPropertiesSection(widget.editorProvider),
109
     );
187
     );
110
   }
188
   }
111
 
189
 
112
-  List<Widget> insertElementSection() {
113
-    return [
114
-      Text('Insert Element'),
115
-      SizedBox(height: 20),
116
-      ElevatedButton(
117
-        onPressed: Provider.of<Editor>(context, listen: false).addTextElement, 
118
-        child: Text('Add Text Element')
119
-      ),
120
-      ElevatedButton(
121
-        onPressed: Provider.of<Editor>(context, listen: false).addTextboxElement, 
122
-        child: Text('Add Textbox Element')
123
-      ),
124
-      SizedBox(height: 24),
125
-
126
-
127
-      // ? Variable Element Section
128
-      Text('Insert Variable Element'),
129
-      SizedBox(height: 24),
130
-
131
-
132
-      ElevatedButton(
133
-        onPressed: Provider.of<Editor>(context, listen: false).addProductNameElement, 
134
-        child: Text('Add Product Name Element')
135
-      ),
136
-      ElevatedButton(
137
-        onPressed: Provider.of<Editor>(context, listen: false).addVariantNameElement, 
138
-        child: Text('Add Variant Name Element')
139
-      ),
140
-      ElevatedButton(
141
-        onPressed: Provider.of<Editor>(context, listen: false).addProductionCodeElement, 
142
-        child: Text('Add Production Code Element')
143
-      ),
144
-      ElevatedButton(
145
-        onPressed: Provider.of<Editor>(context, listen: false).addProductionDateElement, 
146
-        child: Text('Add Production Date Element')
147
-      ),
148
-      ElevatedButton(
149
-        onPressed: Provider.of<Editor>(context, listen: false).addSerialNumberElement, 
150
-        child: Text('Add Serial Number Element')
151
-      ),
152
-    ];
153
-  }
154
-
155
   List<Widget> elementPropertiesSection(Editor editorProvider) {
190
   List<Widget> elementPropertiesSection(Editor editorProvider) {
156
     final element = Provider.of<Editor>(context).selectedElm;
191
     final element = Provider.of<Editor>(context).selectedElm;
157
 
192
 
171
   }
206
   }
172
 
207
 
173
 
208
 
174
-  List<Widget> _variablePropertiesSection(Editor editorProvider, ElementProperty? element) {
209
+  List<Widget> _variablePropertiesSection(Editor editorProvider, ElementState? element) {
175
     return [
210
     return [
176
       Container(
211
       Container(
177
         margin: EdgeInsets.only(bottom: 16),
212
         margin: EdgeInsets.only(bottom: 16),
192
     ];
227
     ];
193
   }
228
   }
194
 
229
 
195
-  List<Widget> _commonPropertiesSection(Editor editorProvider, ElementProperty? element) {
230
+  List<Widget> _commonPropertiesSection(Editor editorProvider, ElementState? element) {
231
+
196
     return [
232
     return [
197
       // ? Value Editor
233
       // ? Value Editor
198
       if([ElementType.text, ElementType.textbox].contains(editorProvider.selectedElm!.type)) TextField(
234
       if([ElementType.text, ElementType.textbox].contains(editorProvider.selectedElm!.type)) TextField(
199
-        focusNode: _focus,
200
         readOnly: element!.isLocked, 
235
         readOnly: element!.isLocked, 
201
         controller: element!.valueController,
236
         controller: element!.valueController,
202
         onTap: editorProvider.enableEdit,
237
         onTap: editorProvider.enableEdit,
203
-        onEditingComplete: () {
204
-          FocusManager.instance.primaryFocus?.unfocus();
238
+        // onEditingComplete: () {
239
+        //   FocusManager.instance.primaryFocus?.unfocus();
205
 
240
 
206
-          editorProvider.disableEdit();
241
+        //   editorProvider.disableEdit();
207
 
242
 
208
-          print('kepenjet');
209
-        },
243
+        //   print('kepenjet');
244
+        // },
210
         onChanged: (newText) {
245
         onChanged: (newText) {
211
-          if (element.lastSavedText != null && newText != element.lastSavedText) {
212
-            EasyDebounce.debounce(
213
-              'text-debounce-toolbar${element.id}',
214
-              const Duration(milliseconds: 500),
215
-              () {
216
-                editorProvider.addUndoEntry(CanvasHistoryModifyType.textEdit, editorProvider.elementProperties);
217
-                element.lastSavedText = newText;
218
-              },
219
-              
220
-            );
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 ?? '';
221
 
250
 
222
-          }
251
+            widget.editorProvider.setValueOnStartEditing(newText);
252
+          });
223
 
253
 
224
 
254
 
225
           setState(() {});
255
           setState(() {});
243
   }
273
   }
244
 
274
 
245
 
275
 
246
-  Widget _buildFontResizerWidget(ElementProperty? element) {
276
+  Widget _buildFontResizerWidget(ElementState? element) {
247
     return Row(
277
     return Row(
248
       children: [
278
       children: [
249
         DropdownButton<int>(
279
         DropdownButton<int>(
266
     );
296
     );
267
   }
297
   }
268
 
298
 
269
-  Widget _buildQrResizerWidget(ElementProperty? element) {
299
+  Widget _buildQrResizerWidget(ElementState? element) {
270
     return  Row(
300
     return  Row(
271
       children: [
301
       children: [
272
         DropdownButton<int>(
302
         DropdownButton<int>(