ANTIPALSU Label template editor using flutter

editor.dart 20KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829
  1. import 'dart:convert';
  2. import 'dart:developer';
  3. import 'package:defer_pointer/defer_pointer.dart';
  4. import 'package:flutter/material.dart';
  5. import 'package:flutter_canvas_editor/history.dart';
  6. import 'package:flutter_canvas_editor/style/canvas_style.dart';
  7. import 'package:flutter_canvas_editor/widgets/elements.dart';
  8. import 'package:toastification/toastification.dart';
  9. import 'package:uuid/uuid.dart';
  10. import 'package:collection/collection.dart';
  11. import '../main.dart';
  12. var uuid = Uuid();
  13. class Editor extends ChangeNotifier {
  14. String editorVersion = '0.1m';
  15. String? selectedElmId;
  16. bool isEditing = false;
  17. final textboxResizerDeferredPointerHandlerLink = DeferredPointerHandlerLink();
  18. // ? Canvas State
  19. double canvasScale = 1.0;
  20. final CanvasProperty canvasProperty = CanvasProperty(
  21. width: 780,
  22. height: 1000,
  23. );
  24. TransformationController canvasTransformationController = TransformationController();
  25. void setCanvasTransformationInitialZoom(BuildContext context) {
  26. final deviceWidth = MediaQuery.of(context).size.width;
  27. canvasScale = deviceWidth / canvasProperty.width * 0.9;
  28. canvasTransformationController.value = Matrix4.identity()..scale(canvasScale);
  29. }
  30. void resetCanvasTransformationScale() {
  31. print('canvas scale $canvasScale');
  32. canvasTransformationController.value = Matrix4.identity()..scale(canvasScale);
  33. }
  34. void disposeCanvasTransformationController() {
  35. canvasTransformationController.dispose();
  36. }
  37. List<ElementProperty> elementProperties = [
  38. ElementProperty(
  39. id: uuid.v4(),
  40. valueController: TextEditingController(text: '{{QRCODE}}'),
  41. type: ElementType.qr,
  42. position: ElementPosition(top: 0, left: 0),
  43. width: 80,
  44. quarterTurns: 0,
  45. elementKey: GlobalKey(),
  46. qrScale: 3
  47. )
  48. ];
  49. // ? History stack
  50. List<CanvasHistory> undoStack = [];
  51. List<List<ElementProperty>> redoStack = [];
  52. void addUndoEntry(CanvasHistoryModifyType canvasHistoryType, List<ElementProperty> elementProperties) {
  53. undoStack.add(CanvasHistory(canvasHistoryType, _cloneCurrentState(elementProperties)));
  54. redoStack.clear();
  55. notifyListeners();
  56. }
  57. void undo() {
  58. addRedoEntry(elementProperties);
  59. // apply to current state
  60. if (undoStack.last.type == CanvasHistoryModifyType.textEdit && undoStack.last.getState != elementProperties) {
  61. undoStack.removeLast();
  62. }
  63. elementProperties = undoStack.last.getState;
  64. // unselect element
  65. if (elementProperties.firstWhereOrNull((e) => e.id == selectedElmId) == null) {
  66. unSelectElm();
  67. }
  68. undoStack.removeLast();
  69. notifyListeners();
  70. }
  71. void addRedoEntry(List<ElementProperty> elementProperties) {
  72. redoStack.add(_cloneCurrentState(elementProperties));
  73. notifyListeners();
  74. }
  75. void redo() {
  76. undoStack.add(CanvasHistory(CanvasHistoryModifyType.redo, _cloneCurrentState(elementProperties)));
  77. // apply to current state
  78. elementProperties = redoStack.last;
  79. redoStack.removeLast();
  80. notifyListeners();
  81. }
  82. List<ElementProperty> _cloneCurrentState(List<ElementProperty> elementProperties) {
  83. List<ElementProperty> clonedElementProperties = elementProperties.map((elementProperty) {
  84. return ElementProperty(
  85. id: elementProperty.id,
  86. valueController: TextEditingController(text: elementProperty.valueController.text),
  87. type: elementProperty.type,
  88. position: ElementPosition(top: elementProperty.position.top, left: elementProperty.position.left),
  89. width: elementProperty.width,
  90. quarterTurns: elementProperty.quarterTurns,
  91. elementKey: elementProperty.elementKey,
  92. qrScale: elementProperty.qrScale,
  93. fontScale: elementProperty.fontScale,
  94. variableType: elementProperty.variableType,
  95. isLocked: elementProperty.isLocked
  96. );
  97. }).toList();
  98. return clonedElementProperties;
  99. }
  100. // ? udpate canvas
  101. void updateCanvasProperty(BuildContext context, double width, double height) {
  102. canvasProperty.height = height;
  103. canvasProperty.width = width;
  104. undoStack.clear();
  105. redoStack.clear();
  106. _adjustOutOfBoundElement();
  107. setCanvasTransformationInitialZoom(context);
  108. notifyListeners();
  109. }
  110. void _adjustOutOfBoundElement() {
  111. for (var element in elementProperties) {
  112. bool isOutOfBoundFromTop = (element.position.top + 10) > canvasProperty.height;
  113. bool isOutOfBoundFromLeft = (element.position.left + 10) > canvasProperty.width;
  114. // if (isOutOfBoundFromTop | isOutOfBoundFromLeft) {
  115. // element.position.top = 0;
  116. // element.position.left = 0;
  117. // }
  118. if (isOutOfBoundFromTop) {
  119. element.position.top = canvasProperty.height - (element.elementKey.currentContext?.size?.height ?? 0);
  120. }
  121. if (isOutOfBoundFromLeft) {
  122. element.position.left = canvasProperty.width - (element.elementKey.currentContext?.size?.width ?? 0);
  123. }
  124. }
  125. }
  126. void populateElement(List<ElementProperty> elementProperties) {
  127. this.elementProperties.addAll(elementProperties);
  128. // _reservedIdUntil = this.elementProperties.length - 1;
  129. notifyListeners();
  130. }
  131. void addTextElement(){
  132. String id = uuid.v4();
  133. ElementProperty element = ElementProperty(
  134. id: id,
  135. valueController: TextEditingController(text: 'Double tap to edit text'),
  136. type: ElementType.text,
  137. position: ElementPosition(top: 0, left: 0),
  138. // size: ElementSize(width: 20, height: 70),
  139. width: 20,
  140. elementKey: GlobalKey(),
  141. quarterTurns: 0
  142. );
  143. // Save History
  144. addUndoEntry(CanvasHistoryModifyType.add, elementProperties);
  145. elementProperties.add(element);
  146. // _reservedIdUntil = elementProperties.length - 1;r
  147. notifyListeners();
  148. selectElmById(id);
  149. }
  150. void addTextboxElement() {
  151. String id = uuid.v4();
  152. ElementProperty element = ElementProperty(
  153. id: id,
  154. valueController: TextEditingController(text: 'Double tap to edit text'),
  155. type: ElementType.textbox,
  156. position: ElementPosition(top: 0, left: 0),
  157. width: 70,
  158. quarterTurns: 0,
  159. elementKey: GlobalKey()
  160. );
  161. // Save History
  162. addUndoEntry(CanvasHistoryModifyType.add, elementProperties);
  163. elementProperties.add(element);
  164. notifyListeners();
  165. selectElmById(id);
  166. }
  167. // ? Variable Element
  168. void addProductNameElement() {
  169. String id = uuid.v4();
  170. ElementProperty element = ElementProperty(
  171. id: id,
  172. valueController: TextEditingController(text: '{{PRODUCTNAME}}'),
  173. type: ElementType.textbox,
  174. variableType: ElementVariableType.productName,
  175. position: ElementPosition(top: 0, left: 0),
  176. width: 250,
  177. quarterTurns: 0,
  178. elementKey: GlobalKey()
  179. );
  180. // Save History
  181. addUndoEntry(CanvasHistoryModifyType.add, elementProperties);
  182. elementProperties.add(element);
  183. notifyListeners();
  184. selectElmById(id);
  185. }
  186. void addVariantNameElement() {
  187. String id = uuid.v4();
  188. ElementProperty element = ElementProperty(
  189. id: id,
  190. valueController: TextEditingController(text: '{{VARIANTNAME}}'),
  191. type: ElementType.textbox,
  192. variableType: ElementVariableType.variantName,
  193. position: ElementPosition(top: 0, left: 0),
  194. width: 250,
  195. quarterTurns: 0,
  196. elementKey: GlobalKey()
  197. );
  198. // Save History
  199. addUndoEntry(CanvasHistoryModifyType.add, elementProperties);
  200. elementProperties.add(element);
  201. notifyListeners();
  202. selectElmById(id);
  203. }
  204. void addProductionCodeElement() {
  205. String id = uuid.v4();
  206. ElementProperty element = ElementProperty(
  207. id: id,
  208. valueController: TextEditingController(text: '{{PRODUCTIONCODE}}'),
  209. type: ElementType.textbox,
  210. variableType: ElementVariableType.productionCode,
  211. position: ElementPosition(top: 0, left: 0),
  212. width: 250,
  213. quarterTurns: 0,
  214. elementKey: GlobalKey()
  215. );
  216. // Save History
  217. addUndoEntry(CanvasHistoryModifyType.add, elementProperties);
  218. elementProperties.add(element);
  219. notifyListeners();
  220. selectElmById(id);
  221. }
  222. void addProductionDateElement() {
  223. String id = uuid.v4();
  224. ElementProperty element = ElementProperty(
  225. id: id,
  226. valueController: TextEditingController(text: '{{PRODUCTIONDATE}}'),
  227. type: ElementType.text,
  228. variableType: ElementVariableType.productionDate,
  229. position: ElementPosition(top: 0, left: 0),
  230. width: 250,
  231. quarterTurns: 0,
  232. elementKey: GlobalKey()
  233. );
  234. // Save History
  235. addUndoEntry(CanvasHistoryModifyType.add, elementProperties);
  236. elementProperties.add(element);
  237. notifyListeners();
  238. selectElmById(id);
  239. }
  240. void addSerialNumberElement() {
  241. String id = uuid.v4();
  242. ElementProperty element = ElementProperty(
  243. id: id,
  244. valueController: TextEditingController(text: '{{SERIALNUMBER}}'),
  245. type: ElementType.text,
  246. variableType: ElementVariableType.serialNumber,
  247. position: ElementPosition(top: 0, left: 0),
  248. width: 250,
  249. quarterTurns: 0,
  250. elementKey: GlobalKey()
  251. );
  252. // Save History
  253. addUndoEntry(CanvasHistoryModifyType.add, elementProperties);
  254. elementProperties.add(element);
  255. notifyListeners();
  256. selectElmById(id);
  257. }
  258. void updateElmPosition(Offset offset) {
  259. ElementProperty? element = selectedElm;
  260. if (element == null) return;
  261. // element.position = ElementPosition(top: offset.dy, left: offset.dx);
  262. print(offset.dx);
  263. element.position.top += offset.dy.round();
  264. element.position.left += offset.dx.round();
  265. notifyListeners();
  266. }
  267. void updateElmWitdh(double width) {
  268. ElementProperty? element = selectedElm;
  269. if (element == null) return;
  270. element.width = width;
  271. notifyListeners();
  272. }
  273. void toggleLockElement() {
  274. if (selectedElm == null) return;
  275. // ? Save History
  276. addUndoEntry(CanvasHistoryModifyType.lock, elementProperties);
  277. selectedElm!.isLocked = !selectedElm!.isLocked;
  278. notifyListeners();
  279. }
  280. void selectElmById(String id) {
  281. selectedElmId = id;
  282. notifyListeners();
  283. }
  284. void enableEdit() {
  285. if (isVariableElement) return;
  286. if (selectedElm!.isLocked) {
  287. _showLockedToast('Cant modify locked element');
  288. return;
  289. }
  290. isEditing = true;
  291. notifyListeners();
  292. }
  293. void disableEdit() {
  294. isEditing = false;
  295. notifyListeners();
  296. }
  297. void unSelectElm() {
  298. selectedElmId = null;
  299. isEditing = false;
  300. notifyListeners();
  301. }
  302. bool isSelected(String id) {
  303. return selectedElmId == id;
  304. }
  305. // ? Getters
  306. String get selectedElmType {
  307. if (elementProperties.isNotEmpty && selectedElmId != null) {
  308. final selectedElm = elementProperties.firstWhere((element) => element.id == selectedElmId);
  309. return elementGetter(selectedElm.type);
  310. }
  311. return '';
  312. }
  313. ElementProperty? get selectedElm {
  314. if (elementProperties.isNotEmpty && selectedElmId != null) {
  315. return elementProperties.firstWhere((element) => element.id == selectedElmId);
  316. }
  317. return null;
  318. }
  319. GlobalKey? get selectedElmKey {
  320. if (elementProperties.isNotEmpty && selectedElmId != null) {
  321. return elementProperties.firstWhere((element) => element.id == selectedElmId).elementKey;
  322. }
  323. return null;
  324. }
  325. bool get shouldShowFontResizer {
  326. if (selectedElm == null) return false;
  327. if (![ElementType.text, ElementType.textbox].contains(selectedElm!.type)) return false;
  328. return true;
  329. }
  330. bool get shouldShowQrResizer {
  331. if (selectedElm == null) return false;
  332. if (selectedElm!.type != ElementType.qr) return false;
  333. return true;
  334. }
  335. bool get shouldShowDeleteElementButton {
  336. if (selectedElm == null) return false;
  337. if ([ElementType.qr].contains(selectedElm!.type)) return false;
  338. return true;
  339. }
  340. bool get isVariableElement {
  341. if (selectedElm == null) return false;
  342. if (selectedElm!.variableType == null) return false;
  343. return true;
  344. }
  345. // ? Element overlay
  346. bool shouldShowTextboxResizer(String elmId) {
  347. if (selectedElmId == null) return false;
  348. if (selectedElmId != elmId ) return false;
  349. if (selectedElm!.type != ElementType.textbox) return false;
  350. return true;
  351. }
  352. bool shouldShowOverlay(String elmId) {
  353. if (selectedElmId == null) return false;
  354. if (selectedElmId != elmId ) return false;
  355. if (selectedElm!.type == ElementType.qr) return false;
  356. return true;
  357. }
  358. // Toolbar
  359. bool get insertElementMode{
  360. if (selectedElm == null) return true;
  361. return false;
  362. }
  363. /// Can only rotate [ElementType.text, ElementType.textBox]
  364. void rotate() {
  365. ElementProperty? element = selectedElm;
  366. if (element == null) return;
  367. if (element.isLocked) {
  368. _showLockedToast('Cant rotate locked element');
  369. return;
  370. }
  371. if (![ElementType.text, ElementType.textbox].contains(element.type)) return;
  372. // ? Save history
  373. addUndoEntry(CanvasHistoryModifyType.rotate, elementProperties);
  374. if (element.type == ElementType.text) _rotateText(element);
  375. if (element.type == ElementType.textbox) _rotateTextBox(element);
  376. // Adjust Size
  377. // double currentElementHeight = selectedElmKey!.currentContext!.size!.height;
  378. // double currentElementWidth = selectedElmKey!.currentContext!.size!.width;
  379. // selectedElmKey!.currentContext!.size!.height = currentElementWidth;
  380. // selectedElmKey!.currentContext!.size!.width = currentElementHeight;
  381. notifyListeners();
  382. }
  383. void _rotateText(ElementProperty element) {
  384. if (element.quarterTurns < 3) {
  385. element.quarterTurns += 1;
  386. } else {
  387. element.quarterTurns = 0;
  388. }
  389. }
  390. void _rotateTextBox(ElementProperty element) {
  391. if (element.quarterTurns == 0) {
  392. element.quarterTurns = 3;
  393. } else {
  394. element.quarterTurns = 0;
  395. }
  396. }
  397. // FontSize Handler
  398. void changeFontSize(int? fontSize) {
  399. if (fontSize == null) return;
  400. if (selectedElm == null) return;
  401. if (selectedElm!.isLocked) {
  402. _showLockedToast('Cant modify locked element');
  403. return;
  404. }
  405. // ? Save History
  406. addUndoEntry(CanvasHistoryModifyType.resize, elementProperties);
  407. selectedElm!.fontScale = fontSize;
  408. notifyListeners();
  409. }
  410. void incrementFontSize() {
  411. if (selectedElm == null) return;
  412. final incrementTo = selectedElm!.fontScale + 1;
  413. if (selectedElm!.isLocked) {
  414. _showLockedToast('Cant modify locked element');
  415. return;
  416. }
  417. // check if value is allowed for resize
  418. if (CanvasStyle.fontSizeMap.containsKey(incrementTo)) {
  419. // ? Save History
  420. addUndoEntry(CanvasHistoryModifyType.resize, elementProperties);
  421. selectedElm!.fontScale = incrementTo;
  422. print('kepenjet increase');
  423. } else {
  424. print('cant increment');
  425. }
  426. notifyListeners();
  427. }
  428. void decrementFontSize() {
  429. if (selectedElm == null) return;
  430. final decrementTo = selectedElm!.fontScale - 1;
  431. if (selectedElm!.isLocked) {
  432. _showLockedToast('Cant modify locked element');
  433. return;
  434. }
  435. // check if value is allowed for resize
  436. if (CanvasStyle.fontSizeMap.containsKey(decrementTo)) {
  437. // ? Save History
  438. addUndoEntry(CanvasHistoryModifyType.resize, elementProperties);
  439. selectedElm!.fontScale = decrementTo;
  440. } else {
  441. print('cant decrement');
  442. }
  443. notifyListeners();
  444. }
  445. // Qr Size Handler
  446. void changeQrSize(int? fontSize) {
  447. if (fontSize == null) return;
  448. if (selectedElm == null) return;
  449. if (selectedElm!.isLocked) {
  450. _showLockedToast('Cant modify locked element');
  451. return;
  452. }
  453. selectedElm!.qrScale = fontSize;
  454. notifyListeners();
  455. }
  456. void incrementQrSize() {
  457. if (selectedElm == null) return;
  458. if (selectedElm!.type != ElementType.qr) return;
  459. final incrementTo = selectedElm!.qrScale! + 1;
  460. if (selectedElm!.isLocked) {
  461. _showLockedToast('Cant modify locked element');
  462. return;
  463. }
  464. // check if value is allowed for resize
  465. if (CanvasStyle.qrSizeMap.containsKey(incrementTo)) {
  466. selectedElm!.qrScale = incrementTo;
  467. } else {
  468. print('cant increment');
  469. }
  470. notifyListeners();
  471. }
  472. void decrementQrSize() {
  473. if (selectedElm == null) return;
  474. if (selectedElm!.type != ElementType.qr) return;
  475. final decrementTo = selectedElm!.qrScale! - 1;
  476. if (selectedElm!.isLocked) {
  477. _showLockedToast('Cant modify locked element');
  478. return;
  479. }
  480. // check if value is allowed for resize
  481. if (CanvasStyle.qrSizeMap.containsKey(decrementTo)) {
  482. selectedElm!.qrScale = decrementTo;
  483. } else {
  484. print('cant decrement');
  485. }
  486. notifyListeners();
  487. }
  488. // Delete Element
  489. Future<void> deleteElement(BuildContext context) async {
  490. if (selectedElm == null) return;
  491. if (selectedElm!.isLocked) {
  492. _showLockedToast('Cant delete locked element');
  493. return;
  494. }
  495. final shouldDelete = await showDialog(
  496. context: context,
  497. builder: (context) {
  498. return AlertDialog(
  499. title: Text('Delete Element ?'),
  500. content: Text('Are you sure want to delete this element ?'),
  501. actions: [
  502. TextButton(
  503. onPressed: () => Navigator.pop(context, false),
  504. child: Text('Cancel')
  505. ),
  506. TextButton(
  507. onPressed: () => Navigator.pop(context, true),
  508. child: Text('Delete')
  509. ),
  510. ],
  511. );
  512. },
  513. );
  514. if (!shouldDelete) return;
  515. // ? Save History
  516. addUndoEntry(CanvasHistoryModifyType.remove, elementProperties);
  517. elementProperties.removeWhere((e) => e.id == selectedElm!.id);
  518. unSelectElm();
  519. notifyListeners();
  520. }
  521. // ? snapping element
  522. // Offset? snapToElement(Offset newPosition) {
  523. // for (var element in elementProperties) {
  524. // if ((newPosition.dx - element.))
  525. // }
  526. // }
  527. // ? helper method
  528. void _showLockedToast(String titleText) {
  529. toastification.show(
  530. title: Text(titleText),
  531. description: Text('unlock to change element property'),
  532. closeButtonShowType: CloseButtonShowType.none,
  533. style: ToastificationStyle.minimal,
  534. type: ToastificationType.warning,
  535. autoCloseDuration: const Duration(seconds: 3),
  536. alignment: Alignment.bottomCenter,
  537. dragToClose: true
  538. );
  539. }
  540. // ? Template to JSON
  541. String buildJSON() {
  542. var elementsMap = [];
  543. var canvasResultMap = {
  544. 'title': 'Test',
  545. 'width': (canvasProperty.width / 10).round(),
  546. 'height': (canvasProperty.height / 10).round(),
  547. 'content': {
  548. 'version': editorVersion,
  549. 'elements': []
  550. }
  551. };
  552. for (var element in elementProperties) {
  553. var elementMap = {
  554. 'id': uuid.v4(),
  555. 'content': element.valueController.text,
  556. 'height': element.elementKey.currentContext?.size?.height.round() ?? 0,
  557. 'width': element.elementKey.currentContext?.size?.width.round() ?? 0,
  558. 'position': {
  559. 'top': element.position.top.round(),
  560. 'left': element.position.left.round()
  561. },
  562. 'size': _getElementSizeResult(element),
  563. 'rotation': _getElementRotationResult(element.quarterTurns),
  564. 'type': _getElementTypeResult(element)
  565. };
  566. elementsMap.add(elementMap);
  567. }
  568. (canvasResultMap['content'] as Map<String, dynamic>)['elements'] = elementsMap;
  569. final templateJsonResult = jsonEncode(canvasResultMap);
  570. print(templateJsonResult);
  571. log(templateJsonResult);
  572. return templateJsonResult;
  573. }
  574. String _getElementTypeResult(ElementProperty element) {
  575. switch (element.type) {
  576. case ElementType.text:
  577. return 'text';
  578. case ElementType.textbox:
  579. return 'textbox';
  580. case ElementType.qr:
  581. return 'qrcode';
  582. default:
  583. return '';
  584. }
  585. }
  586. int _getElementRotationResult(int quarterTurns) {
  587. switch (quarterTurns) {
  588. case 1:
  589. return 90;
  590. case 2:
  591. return 180;
  592. case 3:
  593. return -90;
  594. default:
  595. return 0;
  596. }
  597. }
  598. int _getElementSizeResult(ElementProperty element) {
  599. if (element.type == ElementType.qr) {
  600. return element.qrScale ?? 1;
  601. }
  602. return element.fontScale;
  603. }
  604. }