popover.dart 7.5 KB

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