import 'dart:convert'; import 'dart:developer'; import 'package:defer_pointer/defer_pointer.dart'; import 'package:flutter/material.dart'; import 'package:flutter_canvas_editor/history.dart'; import 'package:flutter_canvas_editor/style/canvas_style.dart'; import 'package:flutter_canvas_editor/widgets/elements.dart'; import 'package:toastification/toastification.dart'; import 'package:uuid/uuid.dart'; import 'package:collection/collection.dart'; import '../main.dart'; var uuid = Uuid(); class Editor extends ChangeNotifier { String editorVersion = '0.1m'; String? selectedElmId; bool isEditing = false; final textboxResizerDeferredPointerHandlerLink = DeferredPointerHandlerLink(); String? valueOnStartEditing; // ? Canvas State double canvasScale = 1.0; final CanvasProperty canvasProperty = CanvasProperty( width: 780, height: 1000, ); TransformationController canvasTransformationController = TransformationController(); void setCanvasTransformationInitialZoom(BuildContext context) { final deviceWidth = MediaQuery.of(context).size.width; canvasScale = deviceWidth / canvasProperty.width * 0.9; canvasTransformationController.value = Matrix4.identity()..scale(canvasScale); } void resetCanvasTransformationScale() { print('canvas scale $canvasScale'); canvasTransformationController.value = Matrix4.identity()..scale(canvasScale); } void disposeCanvasTransformationController() { canvasTransformationController.dispose(); } // List elementProperties = [ // ElementProperty( // id: uuid.v4(), // valueController: TextEditingController(text: '{{QRCODE}}'), // type: ElementType.qr, // position: ElementPosition(top: 0, left: 0), // width: 80, // quarterTurns: 0, // elementKey: GlobalKey(), // qrScale: 3 // ) // ]; // This list store all changes history, and currentCanvasState (stackState.last) List> stateStack = [ // ? default state [ ElementState( id: uuid.v4(), valueController: TextEditingController(text: '{{QRCODE}}'), type: ElementType.qr, position: ElementPosition(top: 0, left: 0), width: 80, quarterTurns: 0, elementKey: GlobalKey(), qrScale: 3 ) ] ]; // current canvas state List get currentElementsState => stateStack.last; // ? History stack // List undoStack = []; List> redoStack = []; void setNewElementsState(List newElementsState) { _setNewStateTextEdit(newElementsState); stateStack.add(newElementsState); redoStack.clear(); notifyListeners(); } void undo() { addRedoEntry(stateStack.last); // apply to current state // if (undoStack.last.type == CanvasHistoryModifyType.textEdit && undoStack.last.getState != elementProperties) { // undoStack.removeLast(); // } // elementProperties = undoStack.last.getState; stateStack.removeLast(); // unselect element if (currentElementsState.firstWhereOrNull((e) => e.id == selectedElmId) == null) { unSelectElm(); } notifyListeners(); } void addRedoEntry(List elementProperties) { redoStack.add(_cloneElementsState(elementProperties)); notifyListeners(); } void redo() { // undoStack.add(CanvasHistory(CanvasHistoryModifyType.redo, _cloneCurrentState(elementProperties))); stateStack.add(_cloneElementsState(redoStack.last)); // apply to current state // elementProperties = redoStack.last; redoStack.removeLast(); notifyListeners(); } List _cloneElementsState(List elementProperties) { List clonedElementProperties = elementProperties.map((elementProperty) { return ElementState( id: elementProperty.id, valueController: TextEditingController(text: elementProperty.valueController.text), type: elementProperty.type, position: ElementPosition(top: elementProperty.position.top, left: elementProperty.position.left), width: elementProperty.width, quarterTurns: elementProperty.quarterTurns, elementKey: elementProperty.elementKey, qrScale: elementProperty.qrScale, fontScale: elementProperty.fontScale, variableType: elementProperty.variableType, isLocked: elementProperty.isLocked ); }).toList(); return clonedElementProperties; } // ? udpate canvas void updateCanvasProperty(BuildContext context, double width, double height) { canvasProperty.height = height; canvasProperty.width = width; // undoStack.clear(); stateStack = [currentElementsState]; redoStack.clear(); _adjustOutOfBoundElement(); setCanvasTransformationInitialZoom(context); notifyListeners(); } void _adjustOutOfBoundElement() { for (var element in currentElementsState) { bool isOutOfBoundFromTop = (element.position.top + 10) > canvasProperty.height; bool isOutOfBoundFromLeft = (element.position.left + 10) > canvasProperty.width; // if (isOutOfBoundFromTop | isOutOfBoundFromLeft) { // element.position.top = 0; // element.position.left = 0; // } if (isOutOfBoundFromTop) { element.position.top = canvasProperty.height - (element.elementKey.currentContext?.size?.height ?? 0); } if (isOutOfBoundFromLeft) { element.position.left = canvasProperty.width - (element.elementKey.currentContext?.size?.width ?? 0); } } } // ? Primitive Element void addTextElement(){ String id = uuid.v4(); ElementState newElement = ElementState( id: id, valueController: TextEditingController(text: 'Double tap to edit text'), type: ElementType.text, position: ElementPosition(top: 0, left: 0), // size: ElementSize(width: 20, height: 70), width: 20, elementKey: GlobalKey(), quarterTurns: 0 ); // Set State // List newElementsState = [..._cloneElementsState(currentElementsState), newElement]; // setNewElementsState(newElementsState); _setAddNewElementState(newElement); notifyListeners(); selectElmById(id); } void addTextboxElement() { String id = uuid.v4(); ElementState newElement = ElementState( id: id, valueController: TextEditingController(text: 'Double tap to edit text'), type: ElementType.textbox, position: ElementPosition(top: 0, left: 0), width: 70, quarterTurns: 0, elementKey: GlobalKey() ); // Set State _setAddNewElementState(newElement); notifyListeners(); selectElmById(id); } // ? Variable Element void addProductNameElement() { String id = uuid.v4(); ElementState newElement = ElementState( id: id, valueController: TextEditingController(text: '{{PRODUCTNAME}}'), type: ElementType.textbox, variableType: ElementVariableType.productName, position: ElementPosition(top: 0, left: 0), width: 250, quarterTurns: 0, elementKey: GlobalKey() ); // Set State _setAddNewElementState(newElement); notifyListeners(); selectElmById(id); } void addVariantNameElement() { String id = uuid.v4(); ElementState newElement = ElementState( id: id, valueController: TextEditingController(text: '{{VARIANTNAME}}'), type: ElementType.textbox, variableType: ElementVariableType.variantName, position: ElementPosition(top: 0, left: 0), width: 250, quarterTurns: 0, elementKey: GlobalKey() ); // Set State _setAddNewElementState(newElement); notifyListeners(); selectElmById(id); } void addProductionCodeElement() { String id = uuid.v4(); ElementState newElement = ElementState( id: id, valueController: TextEditingController(text: '{{PRODUCTIONCODE}}'), type: ElementType.textbox, variableType: ElementVariableType.productionCode, position: ElementPosition(top: 0, left: 0), width: 250, quarterTurns: 0, elementKey: GlobalKey() ); // Set State _setAddNewElementState(newElement); notifyListeners(); selectElmById(id); } void addProductionDateElement() { String id = uuid.v4(); ElementState newElement = ElementState( id: id, valueController: TextEditingController(text: '{{PRODUCTIONDATE}}'), type: ElementType.text, variableType: ElementVariableType.productionDate, position: ElementPosition(top: 0, left: 0), width: 250, quarterTurns: 0, elementKey: GlobalKey() ); // Save History _setAddNewElementState(newElement); notifyListeners(); selectElmById(id); } void addSerialNumberElement() { String id = uuid.v4(); ElementState newElement = ElementState( id: id, valueController: TextEditingController(text: '{{SERIALNUMBER}}'), type: ElementType.text, variableType: ElementVariableType.serialNumber, position: ElementPosition(top: 0, left: 0), width: 250, quarterTurns: 0, elementKey: GlobalKey() ); // Set State _setAddNewElementState(newElement); notifyListeners(); selectElmById(id); } void updateElmPosition(Offset offset, {double? fixedTop, double? fixedLeft}) { ElementState? element = selectedElm; if (element == null) return; // element.position = ElementPosition(top: offset.dy, left: offset.dx); print(offset.dx); if (fixedTop != null) { element.position.top = fixedTop; } else { element.position.top += offset.dy.round(); } if (fixedLeft != null) { print('fixed left: $fixedLeft'); element.position.left = fixedLeft; } else { element.position.left += offset.dx.round(); } notifyListeners(); } // Reset position after drag end void resetElmPosition(ElementPosition? elementPosition) { ElementState? element = selectedElm; if (element == null) return; if (elementPosition == null) return; element.position = elementPosition; } void moveElement(Offset offset) { log('[MOVING ELEMENT]'); ElementState? element = selectedElm; if (element == null) return; List newElementsState = _cloneElementsState(currentElementsState); var newElement = newElementsState.firstWhere((e) => e.id == selectedElmId); newElement.position.top += offset.dy.round(); newElement.position.left += offset.dx.round(); setNewElementsState(newElementsState); } ElementPosition getClonedElementPosition(ElementState element) { return ElementPosition( top: element.position.top, left: element.position.left ); } void updateElmWitdh(double width) { ElementState? element = selectedElm; if (element == null) return; element.width = width; notifyListeners(); } void commitTextboxResize(double width, double textboxDragStartWidth) { if (selectedElm == null) return; List newElementsState = _cloneElementsState(currentElementsState); var newElement = newElementsState.firstWhere((e) => e.id == selectedElmId); newElement.width = width; // reset previous element _resetPreviousTextboxWidth(selectedElm!, textboxDragStartWidth); setNewElementsState(newElementsState); } void _resetPreviousTextboxWidth(ElementState previousElement, double textboxDragStartWidth) { if (selectedElm == null) return; previousElement.width = textboxDragStartWidth; } void toggleLockElement() { if (selectedElm == null) return; List newElementsState = _cloneElementsState(currentElementsState); var selectedNewElement = newElementsState.firstWhere((e) => e.id == selectedElm!.id); selectedNewElement.isLocked = !selectedNewElement.isLocked; setNewElementsState(newElementsState); // selectedElm!.isLocked = !selectedElm!.isLocked; notifyListeners(); } bool shouldIgnoreTouch(String elementId) { if (elementId == selectedElmId) return false; if (selectedElmId == null) return false; return true; } void selectElmById(String id) { selectedElmId = id; valueOnStartEditing = currentElementsState.firstWhere((e) => e.id == selectedElmId).valueController.text; notifyListeners(); } void enableEdit() { if (isVariableElement) return; if (selectedElm!.isLocked) { _showLockedToast('Cant modify locked element'); return; } isEditing = true; notifyListeners(); } void disableEdit() { isEditing = false; notifyListeners(); } void unSelectElm() { selectedElmId = null; isEditing = false; valueOnStartEditing = null; notifyListeners(); } bool isSelected(String id) { return selectedElmId == id; } // ? Getters String get selectedElmType { if (currentElementsState.isNotEmpty && selectedElmId != null) { final selectedElm = currentElementsState.firstWhere((element) => element.id == selectedElmId); return elementGetter(selectedElm.type); } return ''; } ElementState? get selectedElm { if (currentElementsState.isNotEmpty && selectedElmId != null) { return currentElementsState.firstWhereOrNull((element) => element.id == selectedElmId); } return null; } GlobalKey? get selectedElmKey { if (currentElementsState.isNotEmpty && selectedElmId != null) { return currentElementsState.firstWhere((element) => element.id == selectedElmId).elementKey; } return null; } bool get shouldShowFontResizer { if (selectedElm == null) return false; if (![ElementType.text, ElementType.textbox].contains(selectedElm!.type)) return false; return true; } bool get shouldShowQrResizer { if (selectedElm == null) return false; if (selectedElm!.type != ElementType.qr) return false; return true; } bool get shouldShowDeleteElementButton { if (selectedElm == null) return false; if ([ElementType.qr].contains(selectedElm!.type)) return false; return true; } bool get isVariableElement { if (selectedElm == null) return false; if (selectedElm!.variableType == null) return false; return true; } // ? Element overlay bool shouldShowTextboxResizer(String elmId) { if (selectedElmId == null) return false; if (selectedElmId != elmId ) return false; if (selectedElm!.type != ElementType.textbox) return false; return true; } bool shouldShowOverlay(String elmId) { if (selectedElmId == null) return false; if (selectedElmId != elmId ) return false; if (selectedElm!.type == ElementType.qr) return false; return true; } // Toolbar bool get insertElementMode{ if (selectedElm == null) return true; return false; } /// Can only rotate [ElementType.text, ElementType.textBox] void rotate() { if (selectedElm == null) return; if (selectedElm!.isLocked) { _showLockedToast('Cant rotate locked element'); return; } if (![ElementType.text, ElementType.textbox].contains(selectedElm!.type)) return; List newElementsState = _cloneElementsState(currentElementsState); var newElement = newElementsState.firstWhere((e) => e.id == selectedElmId); if (newElement.type == ElementType.text) _rotateText(newElement); if (newElement.type == ElementType.textbox) _rotateTextBox(newElement); setNewElementsState(newElementsState); // Adjust Size // double currentElementHeight = selectedElmKey!.currentContext!.size!.height; // double currentElementWidth = selectedElmKey!.currentContext!.size!.width; // selectedElmKey!.currentContext!.size!.height = currentElementWidth; // selectedElmKey!.currentContext!.size!.width = currentElementHeight; notifyListeners(); } void editText(TextEditingController controller) { if (selectedElm == null) return; List newElementsState = _cloneElementsState(currentElementsState); var newElement = newElementsState.firstWhere((e) => e.id == selectedElmId); newElement.valueController.text = controller.text; // newElement.valueController.selection = TextSelection.collapsed(offset: controller.selection.base.offset); setNewElementsState(newElementsState); } void setValueOnStartEditing(String text) { valueOnStartEditing = text; } // FontSize Handler void changeFontSize(int? fontSize) { if (fontSize == null) return; if (selectedElm == null) return; if (selectedElm!.isLocked) { _showLockedToast('Cant modify locked element'); return; } List newElementsState = _cloneElementsState(currentElementsState); var newElement = newElementsState.firstWhere((e) => e.id == selectedElmId); newElement.fontScale = fontSize; setNewElementsState(newElementsState); notifyListeners(); } void incrementFontSize() { if (selectedElm == null) return; final incrementTo = selectedElm!.fontScale + 1; if (selectedElm!.isLocked) { _showLockedToast('Cant modify locked element'); return; } // check if value is allowed for resize if (CanvasStyle.fontSizeMap.containsKey(incrementTo)) { // // ? Save History // setNewElementsState(CanvasHistoryModifyType.resize, elementProperties); List newElementsState = _cloneElementsState(currentElementsState); var newElement = newElementsState.firstWhere((e) => e.id == selectedElmId); // selectedElm!.fontScale = incrementTo; newElement.fontScale = incrementTo; setNewElementsState(newElementsState); print('kepenjet increase'); } else { print('cant increment'); } notifyListeners(); } void decrementFontSize() { if (selectedElm == null) return; final decrementTo = selectedElm!.fontScale - 1; if (selectedElm!.isLocked) { _showLockedToast('Cant modify locked element'); return; } // check if value is allowed for resize if (CanvasStyle.fontSizeMap.containsKey(decrementTo)) { List newElementsState = _cloneElementsState(currentElementsState); var newElement = newElementsState.firstWhere((e) => e.id == selectedElmId); newElement.fontScale = decrementTo; setNewElementsState(newElementsState); } else { print('cant decrement'); } } // Qr Size Handler void changeQrSize(int? fontSize) { if (fontSize == null) return; if (selectedElm == null) return; if (selectedElm!.isLocked) { _showLockedToast('Cant modify locked element'); return; } List newElementsState = _cloneElementsState(currentElementsState); var newElement = newElementsState.firstWhere((e) => e.id == selectedElmId); // selectedElm!.qrScale = fontSize; newElement.qrScale = fontSize; setNewElementsState(newElementsState); } void incrementQrSize() { if (selectedElm == null) return; if (selectedElm!.type != ElementType.qr) return; final incrementTo = selectedElm!.qrScale! + 1; if (selectedElm!.isLocked) { _showLockedToast('Cant modify locked element'); return; } // check if value is allowed for resize if (CanvasStyle.qrSizeMap.containsKey(incrementTo)) { List newElementsState = _cloneElementsState(currentElementsState); var newElement = newElementsState.firstWhere((e) => e.id == selectedElmId); // selectedElm!.qrScale = incrementTo; newElement.qrScale = incrementTo; setNewElementsState(newElementsState); } else { print('cant increment'); } } void decrementQrSize() { if (selectedElm == null) return; if (selectedElm!.type != ElementType.qr) return; final decrementTo = selectedElm!.qrScale! - 1; if (selectedElm!.isLocked) { _showLockedToast('Cant modify locked element'); return; } // check if value is allowed for resize if (CanvasStyle.qrSizeMap.containsKey(decrementTo)) { List newElementsState = _cloneElementsState(currentElementsState); var newElement = newElementsState.firstWhere((e) => e.id == selectedElmId); // selectedElm!.qrScale = decrementTo; newElement.qrScale = decrementTo; setNewElementsState(newElementsState); } else { print('cant decrement'); } notifyListeners(); } // Delete Element Future deleteElement(BuildContext context) async { if (selectedElm == null) return; if (selectedElm!.isLocked) { _showLockedToast('Cant delete locked element'); return; } final shouldDelete = await showDialog( context: context, builder: (context) { return AlertDialog( title: Text('Delete Element ?'), content: Text('Are you sure want to delete this element ?'), actions: [ TextButton( onPressed: () => Navigator.pop(context, false), child: Text('Cancel') ), TextButton( onPressed: () => Navigator.pop(context, true), child: Text('Delete') ), ], ); }, ); if (!shouldDelete) return; // // ? Save History // setNewElementsState(CanvasHistoryModifyType.remove, elementProperties); // elementProperties.removeWhere((e) => e.id == selectedElm!.id); List newElementsState = _cloneElementsState(currentElementsState); log('selectedElmId: ${selectedElm!.id}'); for (var i = 0; i < newElementsState.length; i++) { log('element[$i]: ${newElementsState[i].id}'); } newElementsState.removeWhere((e) => e.id == selectedElm!.id); setNewElementsState(newElementsState); unSelectElm(); notifyListeners(); } // ? snapping element // Offset? snapToElement(Offset newPosition) { // for (var element in elementProperties) { // if ((newPosition.dx - element.)) // } // } // ? Template to JSON String buildJSON() { var elementsMap = []; var canvasResultMap = { 'title': 'Test', 'width': (canvasProperty.width / 10).round(), 'height': (canvasProperty.height / 10).round(), 'content': { 'version': editorVersion, 'elements': [] } }; for (var element in currentElementsState) { var elementMap = { 'id': element.id, 'content': element.valueController.text, 'height': element.elementKey.currentContext?.size?.height.round() ?? 0, 'width': element.elementKey.currentContext?.size?.width.round() ?? 0, 'position': { 'top': element.position.top.round(), 'left': element.position.left.round() }, 'size': _getElementSizeResult(element), 'rotation': _getElementRotationResult(element.quarterTurns), 'type': _getElementTypeResult(element) }; elementsMap.add(elementMap); } (canvasResultMap['content'] as Map)['elements'] = elementsMap; final templateJsonResult = jsonEncode(canvasResultMap); print(templateJsonResult); log(templateJsonResult); return templateJsonResult; } String _getElementTypeResult(ElementState element) { switch (element.type) { case ElementType.text: return 'text'; case ElementType.textbox: return 'textbox'; case ElementType.qr: return 'qrcode'; default: return ''; } } int _getElementRotationResult(int quarterTurns) { switch (quarterTurns) { case 1: return 90; case 2: return 180; case 3: return -90; default: return 0; } } int _getElementSizeResult(ElementState element) { if (element.type == ElementType.qr) { return element.qrScale ?? 1; } return element.fontScale; } // ? Helper Method void _setAddNewElementState(ElementState newElement) { List newElementsState = [..._cloneElementsState(currentElementsState), newElement]; setNewElementsState(newElementsState); } void _rotateText(ElementState element) { if (element.quarterTurns < 3) { element.quarterTurns += 1; } else { element.quarterTurns = 0; } } void _rotateTextBox(ElementState element) { if (element.quarterTurns == 0) { element.quarterTurns = 3; } else { element.quarterTurns = 0; } } void _showLockedToast(String titleText) { toastification.show( title: Text(titleText), description: Text('unlock to change element property'), closeButtonShowType: CloseButtonShowType.none, style: ToastificationStyle.minimal, type: ToastificationType.warning, autoCloseDuration: const Duration(seconds: 3), alignment: Alignment.bottomCenter, dragToClose: true ); } void _setNewStateTextEdit(List newElementsState) { log(selectedElm?.id ?? 'null'); if (selectedElm == null) return; var newElement = newElementsState.firstWhereOrNull((e) => e.id == selectedElmId); if (newElement != null && [ElementType.text, ElementType.textbox].contains(newElement.type)) { newElement.valueController.selection = TextSelection.collapsed(offset: selectedElm!.valueController.selection.base.offset); } } // canvas alignment line helper bool shouldShowHorizontalCenterLine = false; void updateShouldShowHorizontalCenterLine(bool value) { shouldShowHorizontalCenterLine = value; notifyListeners(); } bool shouldShowVerticalCenterLine = false; void updateShouldShowVerticalCenterLine(bool value) { shouldShowVerticalCenterLine = value; notifyListeners(); } } enum SetActionType { add, remove, move, resize, lock, rotate, textEdit, redo }