ANTIPALSU Label template editor using flutter

elements.dart 18KB

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