popover.dart 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270
  1. import 'package:appflowy_popover/src/layout.dart';
  2. import 'package:flutter/material.dart';
  3. import 'package:flutter/services.dart';
  4. import 'mask.dart';
  5. import 'mutex.dart';
  6. class PopoverController {
  7. PopoverState? _state;
  8. close() {
  9. _state?.close();
  10. }
  11. show() {
  12. _state?.showOverlay();
  13. }
  14. }
  15. class PopoverTriggerFlags {
  16. static const int none = 0x00;
  17. static const int click = 0x01;
  18. static const int hover = 0x02;
  19. }
  20. enum PopoverDirection {
  21. // Corner aligned with a corner of the SourceWidget
  22. topLeft,
  23. topRight,
  24. bottomLeft,
  25. bottomRight,
  26. center,
  27. // Edge aligned with a edge of the SourceWidget
  28. topWithLeftAligned,
  29. topWithCenterAligned,
  30. topWithRightAligned,
  31. rightWithTopAligned,
  32. rightWithCenterAligned,
  33. rightWithBottomAligned,
  34. bottomWithLeftAligned,
  35. bottomWithCenterAligned,
  36. bottomWithRightAligned,
  37. leftWithTopAligned,
  38. leftWithCenterAligned,
  39. leftWithBottomAligned,
  40. custom,
  41. }
  42. class Popover extends StatefulWidget {
  43. final PopoverController? controller;
  44. /// The offset from the [child] where the popover will be drawn
  45. final Offset? offset;
  46. /// Amount of padding between the edges of the window and the popover
  47. final EdgeInsets? windowPadding;
  48. final Decoration? maskDecoration;
  49. /// The function used to build the popover.
  50. final Widget? Function(BuildContext context) popupBuilder;
  51. /// Specify how the popover can be triggered when interacting with the child
  52. /// by supplying a bitwise-OR combination of one or more [PopoverTriggerFlags]
  53. final int triggerActions;
  54. /// If multiple popovers are exclusive,
  55. /// pass the same mutex to them.
  56. final PopoverMutex? mutex;
  57. /// The direction of the popover
  58. final PopoverDirection direction;
  59. final void Function()? onClose;
  60. final Future<bool> Function()? canClose;
  61. final bool asBarrier;
  62. /// The content area of the popover.
  63. final Widget child;
  64. const Popover({
  65. Key? key,
  66. required this.child,
  67. required this.popupBuilder,
  68. this.controller,
  69. this.offset,
  70. this.maskDecoration = const BoxDecoration(
  71. color: Color.fromARGB(0, 244, 67, 54),
  72. ),
  73. this.triggerActions = 0,
  74. this.direction = PopoverDirection.rightWithTopAligned,
  75. this.mutex,
  76. this.windowPadding,
  77. this.onClose,
  78. this.canClose,
  79. this.asBarrier = false,
  80. }) : super(key: key);
  81. @override
  82. State<Popover> createState() => PopoverState();
  83. }
  84. class PopoverState extends State<Popover> {
  85. static final RootOverlayEntry _rootEntry = RootOverlayEntry();
  86. final PopoverLink popoverLink = PopoverLink();
  87. @override
  88. void initState() {
  89. widget.controller?._state = this;
  90. super.initState();
  91. }
  92. void showOverlay() {
  93. close();
  94. if (widget.mutex != null) {
  95. widget.mutex?.state = this;
  96. }
  97. final shouldAddMask = _rootEntry.isEmpty;
  98. final newEntry = OverlayEntry(builder: (context) {
  99. final children = <Widget>[];
  100. if (shouldAddMask) {
  101. children.add(
  102. PopoverMask(
  103. decoration: widget.maskDecoration,
  104. onTap: () async {
  105. if (!(await widget.canClose?.call() ?? true)) {
  106. return;
  107. }
  108. _removeRootOverlay();
  109. },
  110. ),
  111. );
  112. }
  113. children.add(
  114. PopoverContainer(
  115. direction: widget.direction,
  116. popoverLink: popoverLink,
  117. offset: widget.offset ?? Offset.zero,
  118. windowPadding: widget.windowPadding ?? EdgeInsets.zero,
  119. popupBuilder: widget.popupBuilder,
  120. onClose: () => close(),
  121. onCloseAll: () => _removeRootOverlay(),
  122. ),
  123. );
  124. return FocusScope(
  125. onKey: (node, event) {
  126. if (event is RawKeyDownEvent &&
  127. event.logicalKey == LogicalKeyboardKey.escape) {
  128. _removeRootOverlay();
  129. return KeyEventResult.handled;
  130. }
  131. return KeyEventResult.ignored;
  132. },
  133. child: Stack(children: children),
  134. );
  135. });
  136. _rootEntry.addEntry(context, this, newEntry, widget.asBarrier);
  137. }
  138. void close() {
  139. if (_rootEntry.contains(this)) {
  140. _rootEntry.removeEntry(this);
  141. widget.onClose?.call();
  142. }
  143. }
  144. void _removeRootOverlay() {
  145. _rootEntry.popEntry();
  146. if (widget.mutex?.state == this) {
  147. widget.mutex?.removeState();
  148. }
  149. }
  150. @override
  151. void deactivate() {
  152. close();
  153. super.deactivate();
  154. }
  155. @override
  156. Widget build(BuildContext context) {
  157. return PopoverTarget(
  158. link: popoverLink,
  159. child: _buildChild(context),
  160. );
  161. }
  162. Widget _buildChild(BuildContext context) {
  163. if (widget.triggerActions == 0) {
  164. return widget.child;
  165. }
  166. return MouseRegion(
  167. onEnter: (event) {
  168. if (widget.triggerActions & PopoverTriggerFlags.hover != 0) {
  169. showOverlay();
  170. }
  171. },
  172. child: Listener(
  173. child: widget.child,
  174. onPointerDown: (_) {
  175. if (widget.triggerActions & PopoverTriggerFlags.click != 0) {
  176. showOverlay();
  177. }
  178. },
  179. ),
  180. );
  181. }
  182. }
  183. class PopoverContainer extends StatefulWidget {
  184. final Widget? Function(BuildContext context) popupBuilder;
  185. final PopoverDirection direction;
  186. final PopoverLink popoverLink;
  187. final Offset offset;
  188. final EdgeInsets windowPadding;
  189. final void Function() onClose;
  190. final void Function() onCloseAll;
  191. const PopoverContainer({
  192. Key? key,
  193. required this.popupBuilder,
  194. required this.direction,
  195. required this.popoverLink,
  196. required this.offset,
  197. required this.windowPadding,
  198. required this.onClose,
  199. required this.onCloseAll,
  200. }) : super(key: key);
  201. @override
  202. State<StatefulWidget> createState() => PopoverContainerState();
  203. static PopoverContainerState of(BuildContext context) {
  204. if (context is StatefulElement && context.state is PopoverContainerState) {
  205. return context.state as PopoverContainerState;
  206. }
  207. final PopoverContainerState? result =
  208. context.findAncestorStateOfType<PopoverContainerState>();
  209. return result!;
  210. }
  211. }
  212. class PopoverContainerState extends State<PopoverContainer> {
  213. @override
  214. Widget build(BuildContext context) {
  215. return Focus(
  216. autofocus: true,
  217. child: CustomSingleChildLayout(
  218. delegate: PopoverLayoutDelegate(
  219. direction: widget.direction,
  220. link: widget.popoverLink,
  221. offset: widget.offset,
  222. windowPadding: widget.windowPadding,
  223. ),
  224. child: widget.popupBuilder(context),
  225. ),
  226. );
  227. }
  228. void close() => widget.onClose();
  229. void closeAll() => widget.onCloseAll();
  230. }