ANTIPALSU Label template editor using flutter

elements.dart 22KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621
  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. if (!Provider.of<Editor>(context, listen: false).isSelected(element.id)) {
  201. return;
  202. }
  203. if (element.isLocked) return;
  204. final elmKeyContext = Provider.of<Editor>(context, listen: false).selectedElmKey?.currentContext;
  205. if (elmKeyContext == null) {
  206. debugPrint('WARNING, elmKeyContext not found');
  207. }
  208. bool isElmRotatedVertically = [1,3].contains(element.quarterTurns);
  209. double width = isElmRotatedVertically ? elmKeyContext!.size!.height : elmKeyContext!.size!.width;
  210. double height = isElmRotatedVertically ? elmKeyContext.size!.width : elmKeyContext.size!.height;
  211. double right = element.position.left + width;
  212. double bottom = element.position.top + height;
  213. bool isElmWidthExceedCanvas = width > canvas.width;
  214. bool isElmHeightExceedCanvas = height > canvas.height;
  215. // Check if the object is out of the canvas
  216. if (element.position.top < 0) {
  217. setState(() {
  218. element.position.top = 0;
  219. });
  220. print('object is out of canvas');
  221. return;
  222. }
  223. if (element.position.left < 0) {
  224. setState(() {
  225. element.position.left = 0;
  226. });
  227. print('object is out of canvas');
  228. return;
  229. }
  230. if (!isElmHeightExceedCanvas && bottom > canvas.height) {
  231. setState(() {
  232. element.position.top = (canvas.height - height).roundToDouble() - 1;
  233. });
  234. print('object is out of canvas');
  235. return;
  236. }
  237. if (!isElmWidthExceedCanvas && right > canvas.width) {
  238. setState(() {
  239. element.position.left = (canvas.width - width).roundToDouble() - 1;
  240. });
  241. print('object is out of canvas');
  242. return;
  243. }
  244. Provider.of<Editor>(context, listen: false).updateElmPosition(details.delta);
  245. },
  246. onPanEnd: (details) {
  247. if (!Provider.of<Editor>(context, listen: false).isSelected(element.id)) {
  248. return;
  249. }
  250. if (element.isLocked) return;
  251. // ? Adjust overflow position
  252. ElementPosition adjustedElementPosition = element.position;
  253. final elmKeyContext = element.elementKey.currentContext;
  254. bool isElmRotatedVertically = [1,3].contains(element.quarterTurns);
  255. double width = isElmRotatedVertically ? elmKeyContext!.size!.height : elmKeyContext!.size!.width;
  256. double height = isElmRotatedVertically ? elmKeyContext.size!.width : elmKeyContext.size!.height;
  257. double right = element.position.left + width;
  258. double bottom = element.position.top + height;
  259. if (element.position.top < 0) {
  260. adjustedElementPosition.top = 0;
  261. }
  262. if (element.position.left < 0) {
  263. adjustedElementPosition.left = 0;
  264. }
  265. if ((element.position.top + height) > canvas.height) {
  266. adjustedElementPosition.top = (canvas.height - height).roundToDouble() - 1;
  267. }
  268. if ((element.position.left + width) > canvas.width) {
  269. adjustedElementPosition.left = (canvas.width - width).roundToDouble() - 1;
  270. }
  271. // Provider.of<Editor>(context, listen: false).updateElmPosition(
  272. // Offset(adjustedElementPosition.left - element.position.left, adjustedElementPosition.top - element.position.top),
  273. // // Provider.of<Editor>(context, listen: false).elementProperties
  274. // );
  275. Provider.of<Editor>(context, listen: false).resetElmPosition(_elementDragStartPosition);
  276. Provider.of<Editor>(context, listen: false).moveElement(
  277. Offset(adjustedElementPosition.left - element.position.left, adjustedElementPosition.top - element.position.top)
  278. );
  279. },
  280. child: Stack(
  281. clipBehavior: Clip.none,
  282. children: [
  283. RotatedBox(
  284. // angle: element.quarterTurns * (math.pi / 2),
  285. quarterTurns: element.quarterTurns,
  286. child: Stack(
  287. clipBehavior: Clip.none,
  288. children: [
  289. Container(
  290. width: element.type == ElementType.text
  291. ? null
  292. : element.type == ElementType.qr
  293. ? CanvasStyle.getQrSize(element.qrScale!)
  294. : element.width ,
  295. height: element.type == ElementType.qr
  296. ? CanvasStyle.getQrSize(element.qrScale!)
  297. : null,
  298. // child: Text('Top: ${element.position.top}, Left: ${element.position.left}, isSelected: ${Provider.of<Editor>(context, listen: false).isSelected(element.id)}'),
  299. key: widget.globalKey,
  300. decoration: BoxDecoration(
  301. // color: element.color ?? Colors.blue,
  302. border: Provider.of<Editor>(context, listen: true).isSelected(element.id) ? Border.all(
  303. color:Colors.red,
  304. width: 2,
  305. strokeAlign: BorderSide.strokeAlignOutside,
  306. ) : null,
  307. ),
  308. // ? child element
  309. child: _buildChildElement(element),
  310. ),
  311. // ? Textbox Resizer
  312. if (editorProvider.shouldShowTextboxResizer(element.id)) Positioned(
  313. right: _resizerWidth / -2,
  314. top:_height / 2 - (_resizerHeight / 2),
  315. child: DeferPointer(
  316. link: editorProvider.textboxResizerDeferredPointerHandlerLink,
  317. // paintOnTop: true,
  318. child: Transform.scale(
  319. scale: 1 / _currentScale,
  320. child: Listener(
  321. onPointerDown: (details) {
  322. _textboxDragStartPosition = details.position;
  323. _textboxDragStartWidth = element.width;
  324. },
  325. onPointerMove: (details) {
  326. if (element.isLocked) return;
  327. setState(() {
  328. final selectedElm = Provider.of<Editor>(context, listen: false).selectedElm;
  329. if (selectedElm == null) return;
  330. final elmKeyContext = selectedElm.elementKey.currentContext;
  331. double width = elmKeyContext!.size!.width;
  332. print(MediaQuery.of(context).devicePixelRatio);
  333. var delta;
  334. double canvasWidth = 1.0;
  335. print (_textboxDragStartPosition!.dx);
  336. print (_textboxDragStartPosition!.dy);
  337. // adjust width based on rotation
  338. print('quarter turn: ${element.quarterTurns}');
  339. switch(element.quarterTurns) {
  340. case 0:
  341. delta = details.position.dx - _textboxDragStartPosition!.dx;
  342. canvasWidth = Provider.of<Editor>(context, listen: false).canvasProperty.width;
  343. case 3:
  344. delta = _textboxDragStartPosition!.dy - details.position.dy;
  345. element.position.top -= delta / _currentScale;
  346. canvasWidth = Provider.of<Editor>(context, listen: false).canvasProperty.height;
  347. }
  348. element.width += delta / _currentScale; // Adjust width
  349. print('current scale $_currentScale');
  350. // Enforce minimum size
  351. element.width = element.width.clamp(75.0, canvasWidth);
  352. _textboxDragStartPosition = details.position;
  353. if (width <= 0) return;
  354. Provider.of<Editor>(context, listen: false).updateElmWitdh(element.width);
  355. });
  356. },
  357. onPointerUp: (details) {
  358. editorProvider.commitTextboxResize(element.width, _textboxDragStartWidth ?? 70);
  359. _textboxDragStartPosition = null;
  360. _textboxDragStartWidth = null;
  361. },
  362. child: Container(
  363. decoration: BoxDecoration(
  364. shape: BoxShape.circle,
  365. color: Colors.blue.withOpacity(0.7),
  366. ),
  367. width: _resizerWidth,
  368. height: _resizerHeight,
  369. child: const RotatedBox(
  370. quarterTurns: 1,
  371. child: Icon(
  372. Icons.height,
  373. size: 24,
  374. color: Colors.white,
  375. ),
  376. ),
  377. ),
  378. ),
  379. ),
  380. ),
  381. ),
  382. // if (Provider.of<Editor>(context).selectedElmId == element.id && element.type != ElementType.qr) ... [
  383. // ]
  384. ],
  385. ),
  386. ),
  387. //? Overlay Button
  388. if (editorProvider.shouldShowOverlay(element.id)) Positioned(
  389. top: -60 / _currentScale,
  390. left: 0,
  391. child: DeferPointer(
  392. paintOnTop: true,
  393. child: Transform.scale(
  394. scale: 1 / _currentScale,
  395. alignment: Alignment.topLeft,
  396. child: Row(
  397. children: [
  398. IconButton.filled(
  399. onPressed: () {
  400. print('delete overlay tapped');
  401. editorProvider.deleteElement(context);
  402. },
  403. icon: const Icon(Icons.delete),
  404. color: Theme.of(context).colorScheme.error,
  405. style: ButtonStyle(
  406. backgroundColor: WidgetStatePropertyAll(Theme.of(context).colorScheme.errorContainer)
  407. ),
  408. ),
  409. IconButton.filled(
  410. onPressed: () {
  411. print('rotate overlay tapped');
  412. editorProvider.rotate();
  413. // test
  414. var getBox = element.elementKey.currentContext!.findRenderObject();
  415. },
  416. icon: const Icon(Icons.rotate_90_degrees_cw),
  417. // color: Theme.of(context).colorScheme.error,
  418. style: const ButtonStyle(
  419. // backgroundColor: WidgetStatePropertyAll(Theme.of(context).colorScheme.errorContainer)
  420. ),
  421. ),
  422. IconButton.filled(
  423. onPressed: () {
  424. print('lock overlay tapped');
  425. editorProvider.toggleLockElement();
  426. },
  427. icon: Icon(element.isLocked ? Icons.lock_outline : Icons.lock_open),
  428. // color: element.isLocked ? Theme.of(context).colorScheme.error,
  429. style: ButtonStyle(
  430. backgroundColor: element.isLocked ? WidgetStatePropertyAll(Theme.of(context).colorScheme.error) : null
  431. ),
  432. ),
  433. ],
  434. ),
  435. ),
  436. )
  437. )
  438. ],
  439. ),
  440. ),
  441. ),
  442. );
  443. }
  444. Widget _buildChildElement(ElementState element) {
  445. // ? build QR element
  446. if (element.type == ElementType.qr) {
  447. return const Image(image: AssetImage('asset/images/qr_template.png'));
  448. }
  449. // ? [EDITING] build text field
  450. if (Provider.of<Editor>(context, listen: true).isEditing && (Provider.of<Editor>(context, listen: true).selectedElmId == element.id)) {
  451. return IntrinsicWidth(
  452. child: TextField(
  453. controller: element.valueController,
  454. autofocus: true,
  455. keyboardType: TextInputType.multiline,
  456. enableSuggestions: false,
  457. autocorrect: false,
  458. maxLines: null,
  459. style: CanvasStyle.getTextStyle(element.fontScale),
  460. decoration: const InputDecoration(
  461. isDense: true,
  462. contentPadding: EdgeInsets.zero,
  463. border: InputBorder.none,
  464. ),
  465. onChanged: (newText) {
  466. Debouncer.run(() {
  467. log('[SAVING TO HISTORY]');
  468. Provider.of<Editor>(context, listen: false).editText(element.valueController);
  469. element.valueController.text = Provider.of<Editor>(context, listen: false).valueOnStartEditing ?? '';
  470. Provider.of<Editor>(context, listen: false).setValueOnStartEditing(newText);
  471. });
  472. setState(() {});
  473. },
  474. ),
  475. );
  476. }
  477. // ? build variable element
  478. if (element.variableType != null) {
  479. return Text(
  480. getVariableElmPlaceholder(element.variableType!),
  481. style: CanvasStyle.getTextStyle(element.fontScale, true),
  482. );
  483. }
  484. // ? build text element
  485. return Text(
  486. element.valueController.text,
  487. style: CanvasStyle.getTextStyle(element.fontScale),
  488. );
  489. }
  490. }