import 'dart:developer'; import 'package:defer_pointer/defer_pointer.dart'; import 'package:easy_debounce/easy_debounce.dart'; import 'package:easy_debounce/easy_throttle.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_canvas_editor/history.dart'; import 'package:flutter_canvas_editor/providers/editor.dart'; import 'package:flutter_canvas_editor/utils/debouncer.dart'; import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; import 'dart:math' as math; import '../main.dart'; import '../style/canvas_style.dart'; enum ElementType {text, textbox, qr, image} enum ElementVariableType {productName, variantName, productionCode, productionDate, serialNumber} String elementGetter(ElementType type) { switch (type) { case ElementType.text: return 'Text'; case ElementType.textbox: return 'TextBox'; case ElementType.qr: return 'QR'; case ElementType.image: return 'Image'; default: return ''; } } String getVariableElmPlaceholder(ElementVariableType type) { switch (type) { case ElementVariableType.productName: return 'Product Name*'; case ElementVariableType.variantName: return 'Variant Name*'; case ElementVariableType.productionCode: return 'Production Code*'; case ElementVariableType.productionDate: DateTime now = DateTime.now(); final currentDatetime = DateFormat('yyyy-MM-dd HH:mm:ss').format(now.toLocal()); return '$currentDatetime*'; case ElementVariableType.serialNumber: return 'Serial Number*'; default: return ''; } } class ElementPosition { double top; double left; ElementPosition({required this.top, required this.left}); } class ElementSize { double width; double height; ElementSize({required this.width, required this.height}); } class ElementState { String id; TextEditingController valueController; ElementType type; ElementPosition position; ElementVariableType? variableType; // ElementSize size; double width; int quarterTurns; GlobalKey elementKey; Color? color; int fontScale; int? qrScale; bool isLocked; String? lastSavedText; ElementState({ required this.id, required this.valueController, required this.type, required this.position, // required this.size, required this.width, required this.quarterTurns, required this.elementKey, this.color, this.fontScale = 3, this.qrScale, this.isLocked = false, this.variableType, this.lastSavedText }); } class ElementWidget extends StatefulWidget { final ElementState elementProperty; final CanvasProperty canvasProperty; final GlobalKey globalKey; const ElementWidget({ required this.elementProperty, required this.canvasProperty, required this.globalKey, super.key }); @override State createState() => _ElementWidgetState(); } class _ElementWidgetState extends State { Offset? _textboxDragStartPosition; double? _textboxDragStartWidth; ElementPosition? _elementDragStartPosition; // String? _valueOnStartEditing; // dragable container final double _resizerHeight = 36; final double _resizerWidth = 36; double _height = 0; double _currentScale = 1.0; late TransformationController _transformController; // // TODO: pindhkan ke state element property agar bisa dipake di widget Toolbar // String? _lastSavedText; FocusNode? _focus = FocusNode(); @override void initState() { super.initState(); // _valueOnStartEditing = widget.elementProperty.valueController.text; WidgetsBinding.instance.addPostFrameCallback((_) => _updateScale); _transformController = Provider.of(context, listen: false).canvasTransformationController; _setInitialScale(); _listenTransformationChanges(); _addFocusNodeListener(); widget.elementProperty.valueController.addListener(() {}); } void _updateScale() { _currentScale = Provider.of(context, listen: false).canvasTransformationController.value.getMaxScaleOnAxis(); setState(() {}); } void _setInitialScale() { _updateScale(); } void _listenTransformationChanges() { _transformController.addListener(_updateScale); } void _removeTransformationListener() { _transformController.removeListener(_updateScale); } void _addFocusNodeListener() { if ([ElementType.text, ElementType.textbox].contains(widget.elementProperty.type)) { _focus!.addListener(_onTextFieldFocusChange); } } void _onTextFieldFocusChange() { if (widget.elementProperty.lastSavedText != widget.elementProperty.valueController.text) { print('executed'); // Provider.of(context, listen: false).setNewElementsState(CanvasHistoryModifyType.textEdit, Provider.of(context, listen: false).elementProperties); widget.elementProperty.lastSavedText = widget.elementProperty.valueController.text; } } void _removeFocusNodeListener() { if ([ElementType.text, ElementType.textbox].contains(widget.elementProperty.type)) { _focus!.removeListener(_onTextFieldFocusChange); } } @override void dispose() { _removeTransformationListener(); _removeFocusNodeListener(); super.dispose(); } @override Widget build(BuildContext context) { final editorProvider = Provider.of(context); ElementState element = widget.elementProperty; final CanvasProperty canvas = widget.canvasProperty; WidgetsBinding.instance.addPostFrameCallback((_) { _height = element.elementKey.currentContext!.size!.height; }); // List currentCanvasState = editorProvider.elementProperties; return Positioned( top: element.position.top, left: element.position.left, child: IgnorePointer( ignoring: editorProvider.shouldIgnoreTouch(element.id), child: GestureDetector( onDoubleTap: () { print('double tap detected'); Provider.of(context, listen: false).selectElmById(element.id); Provider.of(context, listen: false).enableEdit(); }, onTap: () { print('Element Gesture Detector Tapped!'); Provider.of(context, listen: false).selectElmById(element.id); }, onPanStart: (details) { if (!Provider.of(context, listen: false).isSelected(element.id)) { return; } if (element.isLocked) return; log('Pan Start'); // inspect(editorProvider.elementProperties); // editorProvider.setNewElementsState(CanvasHistoryModifyType.move, editorProvider.elementProperties); _elementDragStartPosition = Provider.of(context, listen: false).getClonedElementPosition(element); }, onPanUpdate: (details) { // return if moved element is not selected if (!Provider.of(context, listen: false).isSelected(element.id)) { return; } // return if element is locked if (element.isLocked) return; final elmKeyContext = Provider.of(context, listen: false).selectedElmKey?.currentContext; if (elmKeyContext == null) { debugPrint('WARNING, elmKeyContext not found'); } bool isElmRotatedVertically = [1,3].contains(element.quarterTurns); double width = isElmRotatedVertically ? elmKeyContext!.size!.height : elmKeyContext!.size!.width; double height = isElmRotatedVertically ? elmKeyContext.size!.width : elmKeyContext.size!.height; double right = element.position.left + width; double bottom = element.position.top + height; bool isElmWidthExceedCanvas = width > canvas.width; bool isElmHeightExceedCanvas = height > canvas.height; // ! todo: lanjut fix magnet // * alignment test double alignmentTreshold = 5; log('The center offset of this canvas is ${canvas.getCanvasCenterPoint().dx}, ${canvas.getCanvasCenterPoint().dy}'); log('The center offset of this element is ${width / 2}, ${height/2}'); // log('delta ${details.delta.dx}, ${details.delta.dy}'); log('delta distance ${details.delta.distanceSquared}'); final selectedElm = Provider.of(context, listen: false).selectedElm; final elementCenter = Offset(selectedElm!.position.left + (width / 2), selectedElm!.position.top + (height / 2)); bool shouldAborUpdate = false; final canvasCenter = canvas.getCanvasCenterPoint(); if (details.delta.distance < 2) { if ((canvasCenter.dx - elementCenter.dx).abs() < alignmentTreshold) { shouldAborUpdate = true; Provider.of(context, listen: false).updateElmPosition(details.delta, fixedLeft: canvasCenter.dx - (width / 2)); Provider.of(context, listen: false).updateShouldShowHorizontalCenterLine(true); // return; } } else { Provider.of(context, listen: false).updateShouldShowHorizontalCenterLine(false); } if (details.delta.distance < 2) { if ((canvasCenter.dy - elementCenter.dy).abs() < alignmentTreshold) { shouldAborUpdate = true; Provider.of(context, listen: false).updateElmPosition(details.delta, fixedTop: canvasCenter.dy - (height / 2)); Provider.of(context, listen: false).updateShouldShowVerticalCenterLine(true); } } else { Provider.of(context, listen: false).updateShouldShowVerticalCenterLine(false); } // Check if the object is out of the canvas // ? top side if (element.position.top < 0) { setState(() { element.position.top = 0; }); print('object is out of canvas'); log('Adjusting Top Position'); return; } // ? left side if (element.position.left < 0) { setState(() { element.position.left = 0; }); print('object is out of canvas'); log('Adjusting Left Position'); return; } // ? bottom side if (!isElmHeightExceedCanvas && bottom > canvas.height) { setState(() { element.position.top = (canvas.height - height).roundToDouble() - 1; }); log('Adjusting Bottom Position, if !isElmHeightExceedCanvas'); print('object is out of canvas'); return; } // ? right side if (!isElmWidthExceedCanvas && right > canvas.width) { setState(() { element.position.left = (canvas.width - width).roundToDouble() - 1; }); print('object is out of canvas'); log('Adjusting right position, if isElmHeightExceedCanvas'); return; } if (shouldAborUpdate) return; Provider.of(context, listen: false).updateElmPosition(details.delta); }, onPanEnd: (details) { if (!Provider.of(context, listen: false).isSelected(element.id)) { return; } if (element.isLocked) return; // ? Adjust overflow position ElementPosition adjustedElementPosition = element.position; final elmKeyContext = element.elementKey.currentContext; bool isElmRotatedVertically = [1,3].contains(element.quarterTurns); double width = isElmRotatedVertically ? elmKeyContext!.size!.height : elmKeyContext!.size!.width; double height = isElmRotatedVertically ? elmKeyContext.size!.width : elmKeyContext.size!.height; double right = element.position.left + width; double bottom = element.position.top + height; bool isElmWidthExceedCanvas = width > canvas.width; bool isElmHeightExceedCanvas = height > canvas.height; if (element.position.top < 0) { adjustedElementPosition.top = 0; } if (element.position.left < 0) { adjustedElementPosition.left = 0; } if (!isElmHeightExceedCanvas && (element.position.top + height) > canvas.height) { adjustedElementPosition.top = (canvas.height - height).roundToDouble() - 1; } else if (isElmHeightExceedCanvas && (element.position.top + height) > canvas.height) { adjustedElementPosition.top = 0; } if ((element.position.left + width) > canvas.width) { adjustedElementPosition.left = (canvas.width - width).roundToDouble() - 1; } // Provider.of(context, listen: false).updateElmPosition( // Offset(adjustedElementPosition.left - element.position.left, adjustedElementPosition.top - element.position.top), // // Provider.of(context, listen: false).elementProperties // ); Provider.of(context, listen: false).resetElmPosition(_elementDragStartPosition); Provider.of(context, listen: false).moveElement( Offset(adjustedElementPosition.left - element.position.left, adjustedElementPosition.top - element.position.top) ); // reset canvas alignment helper Provider.of(context, listen: false).updateShouldShowHorizontalCenterLine(false); Provider.of(context, listen: false).updateShouldShowVerticalCenterLine(false); }, child: Stack( clipBehavior: Clip.none, children: [ RotatedBox( // angle: element.quarterTurns * (math.pi / 2), quarterTurns: element.quarterTurns, child: Stack( clipBehavior: Clip.none, children: [ Container( width: element.type == ElementType.text ? null : element.type == ElementType.qr ? CanvasStyle.getQrSize(element.qrScale!) : element.width , height: element.type == ElementType.qr ? CanvasStyle.getQrSize(element.qrScale!) : null, // child: Text('Top: ${element.position.top}, Left: ${element.position.left}, isSelected: ${Provider.of(context, listen: false).isSelected(element.id)}'), key: widget.globalKey, decoration: BoxDecoration( // color: element.color ?? Colors.blue, border: Provider.of(context, listen: true).isSelected(element.id) ? Border.all( color:Colors.red, width: 2, strokeAlign: BorderSide.strokeAlignOutside, ) : null, ), // ? child element child: _buildChildElement(element), ), // ? Textbox Resizer if (editorProvider.shouldShowTextboxResizer(element.id)) Positioned( right: _resizerWidth / -2, top:_height / 2 - (_resizerHeight / 2), child: DeferPointer( link: editorProvider.textboxResizerDeferredPointerHandlerLink, // paintOnTop: true, child: Transform.scale( scale: 1 / _currentScale, child: Listener( onPointerDown: (details) { _textboxDragStartPosition = details.position; _textboxDragStartWidth = element.width; }, onPointerMove: (details) { if (element.isLocked) return; setState(() { final selectedElm = Provider.of(context, listen: false).selectedElm; if (selectedElm == null) return; final elmKeyContext = selectedElm.elementKey.currentContext; double width = elmKeyContext!.size!.width; print(MediaQuery.of(context).devicePixelRatio); var delta; double canvasWidth = 1.0; print (_textboxDragStartPosition!.dx); print (_textboxDragStartPosition!.dy); // adjust width based on rotation print('quarter turn: ${element.quarterTurns}'); switch(element.quarterTurns) { case 0: delta = details.position.dx - _textboxDragStartPosition!.dx; canvasWidth = Provider.of(context, listen: false).canvasProperty.width; case 3: delta = _textboxDragStartPosition!.dy - details.position.dy; element.position.top -= delta / _currentScale; canvasWidth = Provider.of(context, listen: false).canvasProperty.height; } element.width += delta / _currentScale; // Adjust width print('current scale $_currentScale'); // Enforce minimum size element.width = element.width.clamp(75.0, canvasWidth); _textboxDragStartPosition = details.position; if (width <= 0) return; Provider.of(context, listen: false).updateElmWitdh(element.width); }); }, onPointerUp: (details) { editorProvider.commitTextboxResize(element.width, _textboxDragStartWidth ?? 70); _textboxDragStartPosition = null; _textboxDragStartWidth = null; }, child: Container( decoration: BoxDecoration( shape: BoxShape.circle, color: Colors.blue.withOpacity(0.7), ), width: _resizerWidth, height: _resizerHeight, child: const RotatedBox( quarterTurns: 1, child: Icon( Icons.height, size: 24, color: Colors.white, ), ), ), ), ), ), ), // if (Provider.of(context).selectedElmId == element.id && element.type != ElementType.qr) ... [ // ] ], ), ), //? Overlay Button if (editorProvider.shouldShowOverlay(element.id)) Positioned( top: -60 / _currentScale, left: 0, child: DeferPointer( paintOnTop: true, child: Transform.scale( scale: 1 / _currentScale, alignment: Alignment.topLeft, child: Row( children: [ IconButton.filled( onPressed: () { print('delete overlay tapped'); editorProvider.deleteElement(context); }, icon: const Icon(Icons.delete), color: Theme.of(context).colorScheme.error, style: ButtonStyle( backgroundColor: WidgetStatePropertyAll(Theme.of(context).colorScheme.errorContainer) ), ), IconButton.filled( onPressed: () { print('rotate overlay tapped'); editorProvider.rotate(); // test var getBox = element.elementKey.currentContext!.findRenderObject(); }, icon: const Icon(Icons.rotate_90_degrees_cw), // color: Theme.of(context).colorScheme.error, style: const ButtonStyle( // backgroundColor: WidgetStatePropertyAll(Theme.of(context).colorScheme.errorContainer) ), ), IconButton.filled( onPressed: () { print('lock overlay tapped'); editorProvider.toggleLockElement(); }, icon: Icon(element.isLocked ? Icons.lock_outline : Icons.lock_open), // color: element.isLocked ? Theme.of(context).colorScheme.error, style: ButtonStyle( backgroundColor: element.isLocked ? WidgetStatePropertyAll(Theme.of(context).colorScheme.error) : null ), ), ], ), ), ) ) ], ), ), ), ); } Widget _buildChildElement(ElementState element) { // ? build QR element if (element.type == ElementType.qr) { return const Image(image: AssetImage('asset/images/qr_template.png')); } // ? [EDITING] build text field if (Provider.of(context, listen: true).isEditing && (Provider.of(context, listen: true).selectedElmId == element.id)) { return IntrinsicWidth( child: TextField( textInputAction: TextInputAction.done, controller: element.valueController, autofocus: true, keyboardType: TextInputType.multiline, enableSuggestions: false, autocorrect: false, maxLines: null, style: CanvasStyle.getTextStyle(element.fontScale), decoration: const InputDecoration( isDense: true, contentPadding: EdgeInsets.zero, border: InputBorder.none, ), onChanged: (newText) { Debouncer.run(() { log('[SAVING TO HISTORY]'); Provider.of(context, listen: false).editText(element.valueController); element.valueController.text = Provider.of(context, listen: false).valueOnStartEditing ?? ''; Provider.of(context, listen: false).setValueOnStartEditing(newText); }); setState(() {}); }, onEditingComplete: () => Provider.of(context, listen: false).unSelectElm(), ), ); } // ? build variable element if (element.variableType != null) { return Text( getVariableElmPlaceholder(element.variableType!), style: CanvasStyle.getTextStyle(element.fontScale, true), ); } // ? build text element return Text( element.valueController.text, style: CanvasStyle.getTextStyle(element.fontScale), // maxLines: 1, ); } }