ANTIPALSU Label template editor using flutter

elements.dart 25KB

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