2 Commits 678474eed8 ... 56f0bd690d

Author SHA1 Message Date
  Raihan Rizal 56f0bd690d feat: history-undo-redo (refactored-stable) 3 months ago
  Raihan Rizal 02d81a56cf feat: history-undo-redo (unstable) 3 months ago
11 changed files with 1091 additions and 473 deletions
  1. 32 3
      lib/canvas_setup_page.dart
  2. 13 0
      lib/history.dart
  3. 15 14
      lib/main.dart
  4. 389 104
      lib/providers/editor.dart
  5. 60 0
      lib/test.dart
  6. 14 0
      lib/text_debouncer_test.dart
  7. 32 0
      lib/utils/debouncer.dart
  8. 377 286
      lib/widgets/elements.dart
  9. 150 66
      lib/widgets/toolbar.dart
  10. 8 0
      pubspec.lock
  11. 1 0
      pubspec.yaml

+ 32 - 3
lib/canvas_setup_page.dart

@@ -51,23 +51,52 @@ class _CanvasSetupPageState extends State<CanvasSetupPage> {
51 51
 
52 52
               TextFormField(
53 53
                 controller: _widthController,
54
-                decoration: InputDecoration(labelText: 'Width'),
54
+                decoration: InputDecoration(labelText: 'Width (mm)'),
55 55
                 keyboardType: TextInputType.number,
56 56
                 validator: (value) {
57 57
                   if (value == null || value.isEmpty) {
58 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 75
                   return null;
61 76
                 },
62 77
               ),
63 78
               TextFormField(
64 79
                 controller: _heightController,
65
-                decoration: InputDecoration(labelText: 'Height'),
80
+                decoration: InputDecoration(labelText: 'Height (mm)'),
66 81
                 keyboardType: TextInputType.number,
67 82
                 validator: (value) {
68 83
                   if (value == null || value.isEmpty) {
69 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 100
                   return null;
72 101
                 },
73 102
               ),
@@ -81,7 +110,7 @@ class _CanvasSetupPageState extends State<CanvasSetupPage> {
81 110
                       context: context, 
82 111
                       builder: (context) => AlertDialog(
83 112
                         title: Text('Update Label Size'),
84
-                        content: Text('updating to smaller label size may cause element lost, Are you sure want to update label size ?'),
113
+                        content: Text('updating label size will remove all history you have changed, Are you sure want to update label size ?'),
85 114
                         actions: [
86 115
                           TextButton(
87 116
                             onPressed: () => Navigator.pop(context, true), 

+ 13 - 0
lib/history.dart

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

+ 15 - 14
lib/main.dart

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

+ 389 - 104
lib/providers/editor.dart

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

+ 60 - 0
lib/test.dart

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

+ 14 - 0
lib/text_debouncer_test.dart

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

+ 32 - 0
lib/utils/debouncer.dart

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

+ 377 - 286
lib/widgets/elements.dart

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

+ 150 - 66
lib/widgets/toolbar.dart

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

+ 8 - 0
pubspec.lock

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

+ 1 - 0
pubspec.yaml

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