ANTIPALSU Label template editor using flutter

elements.dart 16KB

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