ANTIPALSU Label template editor using flutter

elements.dart 20KB

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