popover.dart 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314
  1. import 'package:appflowy_popover/layout.dart';
  2. import 'package:flutter/gestures.dart';
  3. import 'package:flutter/material.dart';
  4. import 'package:flutter/services.dart';
  5. /// If multiple popovers are exclusive,
  6. /// pass the same mutex to them.
  7. class PopoverMutex {
  8. PopoverState? state;
  9. }
  10. class PopoverController {
  11. PopoverState? state;
  12. close() {
  13. state?.close();
  14. }
  15. show() {
  16. state?.showOverlay();
  17. }
  18. }
  19. class PopoverTriggerActionFlags {
  20. static int click = 0x01;
  21. static int hover = 0x02;
  22. }
  23. enum PopoverDirection {
  24. // Corner aligned with a corner of the SourceWidget
  25. topLeft,
  26. topRight,
  27. bottomLeft,
  28. bottomRight,
  29. center,
  30. // Edge aligned with a edge of the SourceWidget
  31. topWithLeftAligned,
  32. topWithCenterAligned,
  33. topWithRightAligned,
  34. rightWithTopAligned,
  35. rightWithCenterAligned,
  36. rightWithBottomAligned,
  37. bottomWithLeftAligned,
  38. bottomWithCenterAligned,
  39. bottomWithRightAligned,
  40. leftWithTopAligned,
  41. leftWithCenterAligned,
  42. leftWithBottomAligned,
  43. custom,
  44. }
  45. class Popover extends StatefulWidget {
  46. final PopoverController? controller;
  47. final Offset? offset;
  48. final Decoration? maskDecoration;
  49. /// The function used to build the popover.
  50. final Widget? Function(BuildContext context) popupBuilder;
  51. final int triggerActions;
  52. /// If multiple popovers are exclusive,
  53. /// pass the same mutex to them.
  54. final PopoverMutex? mutex;
  55. /// The direction of the popover
  56. final PopoverDirection direction;
  57. final void Function()? onClose;
  58. /// The content area of the popover.
  59. final Widget child;
  60. const Popover({
  61. Key? key,
  62. required this.child,
  63. required this.popupBuilder,
  64. this.controller,
  65. this.offset,
  66. this.maskDecoration,
  67. this.triggerActions = 0,
  68. this.direction = PopoverDirection.rightWithTopAligned,
  69. this.mutex,
  70. this.onClose,
  71. }) : super(key: key);
  72. @override
  73. State<Popover> createState() => PopoverState();
  74. }
  75. class PopoverState extends State<Popover> {
  76. final PopoverLink popoverLink = PopoverLink();
  77. OverlayEntry? _overlayEntry;
  78. bool hasMask = true;
  79. static PopoverState? _popoverWithMask;
  80. @override
  81. void initState() {
  82. widget.controller?.state = this;
  83. super.initState();
  84. }
  85. showOverlay() {
  86. close();
  87. if (widget.mutex != null) {
  88. if (widget.mutex!.state != null && widget.mutex!.state != this) {
  89. widget.mutex!.state!.close();
  90. }
  91. widget.mutex!.state = this;
  92. }
  93. if (_popoverWithMask == null) {
  94. _popoverWithMask = this;
  95. } else {
  96. hasMask = false;
  97. }
  98. final newEntry = OverlayEntry(builder: (context) {
  99. final children = <Widget>[];
  100. if (hasMask) {
  101. children.add(_PopoverMask(
  102. decoration: widget.maskDecoration,
  103. onTap: () => close(),
  104. onExit: () => close(),
  105. ));
  106. }
  107. children.add(PopoverContainer(
  108. direction: widget.direction,
  109. popoverLink: popoverLink,
  110. offset: widget.offset ?? Offset.zero,
  111. popupBuilder: widget.popupBuilder,
  112. onClose: () => close(),
  113. onCloseAll: () => closeAll(),
  114. ));
  115. return Stack(children: children);
  116. });
  117. _overlayEntry = newEntry;
  118. Overlay.of(context)?.insert(newEntry);
  119. }
  120. close() {
  121. if (_overlayEntry != null) {
  122. _overlayEntry!.remove();
  123. _overlayEntry = null;
  124. if (_popoverWithMask == this) {
  125. _popoverWithMask = null;
  126. }
  127. if (widget.onClose != null) {
  128. widget.onClose!();
  129. }
  130. }
  131. if (widget.mutex?.state == this) {
  132. widget.mutex!.state = null;
  133. }
  134. }
  135. closeAll() {
  136. _popoverWithMask?.close();
  137. }
  138. @override
  139. void deactivate() {
  140. close();
  141. super.deactivate();
  142. }
  143. _handleTargetPointerDown(PointerDownEvent event) {
  144. if (widget.triggerActions & PopoverTriggerActionFlags.click != 0) {
  145. showOverlay();
  146. }
  147. }
  148. _handleTargetPointerEnter(PointerEnterEvent event) {
  149. if (widget.triggerActions & PopoverTriggerActionFlags.hover != 0) {
  150. showOverlay();
  151. }
  152. }
  153. _buildContent(BuildContext context) {
  154. if (widget.triggerActions == 0) {
  155. return widget.child;
  156. }
  157. return MouseRegion(
  158. onEnter: _handleTargetPointerEnter,
  159. child: Listener(
  160. onPointerDown: _handleTargetPointerDown,
  161. child: widget.child,
  162. ),
  163. );
  164. }
  165. @override
  166. Widget build(BuildContext context) {
  167. return PopoverTarget(
  168. link: popoverLink,
  169. child: _buildContent(context),
  170. );
  171. }
  172. }
  173. class _PopoverMask extends StatefulWidget {
  174. final void Function() onTap;
  175. final void Function()? onExit;
  176. final Decoration? decoration;
  177. const _PopoverMask(
  178. {Key? key, required this.onTap, this.onExit, this.decoration})
  179. : super(key: key);
  180. @override
  181. State<StatefulWidget> createState() => _PopoverMaskState();
  182. }
  183. class _PopoverMaskState extends State<_PopoverMask> {
  184. @override
  185. void initState() {
  186. HardwareKeyboard.instance.addHandler(_handleGlobalKeyEvent);
  187. super.initState();
  188. }
  189. bool _handleGlobalKeyEvent(KeyEvent event) {
  190. if (event.logicalKey == LogicalKeyboardKey.escape) {
  191. if (widget.onExit != null) {
  192. widget.onExit!();
  193. }
  194. return true;
  195. }
  196. return false;
  197. }
  198. @override
  199. void deactivate() {
  200. HardwareKeyboard.instance.removeHandler(_handleGlobalKeyEvent);
  201. super.deactivate();
  202. }
  203. @override
  204. Widget build(BuildContext context) {
  205. return GestureDetector(
  206. onTap: widget.onTap,
  207. child: Container(
  208. // decoration: widget.decoration,
  209. decoration: widget.decoration ??
  210. const BoxDecoration(
  211. color: Color.fromARGB(0, 244, 67, 54),
  212. ),
  213. ),
  214. );
  215. }
  216. }
  217. class PopoverContainer extends StatefulWidget {
  218. final Widget? Function(BuildContext context) popupBuilder;
  219. final PopoverDirection direction;
  220. final PopoverLink popoverLink;
  221. final Offset offset;
  222. final void Function() onClose;
  223. final void Function() onCloseAll;
  224. const PopoverContainer({
  225. Key? key,
  226. required this.popupBuilder,
  227. required this.direction,
  228. required this.popoverLink,
  229. required this.offset,
  230. required this.onClose,
  231. required this.onCloseAll,
  232. }) : super(key: key);
  233. @override
  234. State<StatefulWidget> createState() => PopoverContainerState();
  235. }
  236. class PopoverContainerState extends State<PopoverContainer> {
  237. @override
  238. Widget build(BuildContext context) {
  239. return CustomSingleChildLayout(
  240. delegate: PopoverLayoutDelegate(
  241. direction: widget.direction,
  242. link: widget.popoverLink,
  243. offset: widget.offset,
  244. ),
  245. child: widget.popupBuilder(context),
  246. );
  247. }
  248. close() => widget.onClose();
  249. closeAll() => widget.onCloseAll();
  250. static PopoverContainerState of(BuildContext context) {
  251. if (context is StatefulElement && context.state is PopoverContainerState) {
  252. return context.state as PopoverContainerState;
  253. }
  254. final PopoverContainerState? result =
  255. context.findAncestorStateOfType<PopoverContainerState>();
  256. return result!;
  257. }
  258. }