inline_actions_menu.dart 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  1. import 'package:appflowy/plugins/inline_actions/inline_actions_result.dart';
  2. import 'package:appflowy/plugins/inline_actions/inline_actions_service.dart';
  3. import 'package:appflowy/plugins/inline_actions/widgets/inline_actions_handler.dart';
  4. import 'package:appflowy_editor/appflowy_editor.dart';
  5. import 'package:flutter/material.dart';
  6. abstract class InlineActionsMenuService {
  7. InlineActionsMenuStyle get style;
  8. void show();
  9. void dismiss();
  10. }
  11. class InlineActionsMenu extends InlineActionsMenuService {
  12. InlineActionsMenu({
  13. required this.context,
  14. required this.editorState,
  15. required this.service,
  16. required this.initialResults,
  17. required this.style,
  18. });
  19. final BuildContext context;
  20. final EditorState editorState;
  21. final InlineActionsService service;
  22. final List<InlineActionsResult> initialResults;
  23. @override
  24. final InlineActionsMenuStyle style;
  25. OverlayEntry? _menuEntry;
  26. bool selectionChangedByMenu = false;
  27. @override
  28. void dismiss() {
  29. if (_menuEntry != null) {
  30. editorState.service.keyboardService?.enable();
  31. editorState.service.scrollService?.enable();
  32. }
  33. _menuEntry?.remove();
  34. _menuEntry = null;
  35. // workaround: SelectionService has been released after hot reload.
  36. final isSelectionDisposed =
  37. editorState.service.selectionServiceKey.currentState == null;
  38. if (!isSelectionDisposed) {
  39. final selectionService = editorState.service.selectionService;
  40. selectionService.currentSelection.removeListener(_onSelectionChange);
  41. }
  42. }
  43. void _onSelectionUpdate() => selectionChangedByMenu = true;
  44. @override
  45. void show() {
  46. WidgetsBinding.instance.addPostFrameCallback((_) => _show());
  47. }
  48. void _show() {
  49. dismiss();
  50. final selectionService = editorState.service.selectionService;
  51. final selectionRects = selectionService.selectionRects;
  52. if (selectionRects.isEmpty) {
  53. return;
  54. }
  55. const double menuHeight = 200.0;
  56. const Offset menuOffset = Offset(0, 10);
  57. final Offset editorOffset =
  58. editorState.renderBox?.localToGlobal(Offset.zero) ?? Offset.zero;
  59. final Size editorSize = editorState.renderBox!.size;
  60. // Default to opening the overlay below
  61. Alignment alignment = Alignment.topLeft;
  62. final firstRect = selectionRects.first;
  63. Offset offset = firstRect.bottomRight + menuOffset;
  64. // Show above
  65. if (offset.dy + menuHeight >= editorOffset.dy + editorSize.height) {
  66. offset = firstRect.topRight - menuOffset;
  67. alignment = Alignment.bottomLeft;
  68. offset = Offset(
  69. offset.dx,
  70. MediaQuery.of(context).size.height - offset.dy,
  71. );
  72. }
  73. // Show on the left
  74. if (offset.dx > editorSize.width / 2) {
  75. alignment = alignment == Alignment.topLeft
  76. ? Alignment.topRight
  77. : Alignment.bottomRight;
  78. offset = Offset(
  79. editorSize.width - offset.dx,
  80. offset.dy,
  81. );
  82. }
  83. final (left, top, right, bottom) = _getPosition(alignment, offset);
  84. _menuEntry = OverlayEntry(
  85. builder: (context) => SizedBox(
  86. height: editorSize.height,
  87. width: editorSize.width,
  88. // GestureDetector handles clicks outside of the context menu,
  89. // to dismiss the context menu.
  90. child: GestureDetector(
  91. behavior: HitTestBehavior.opaque,
  92. onTap: dismiss,
  93. child: Stack(
  94. children: [
  95. Positioned(
  96. top: top,
  97. bottom: bottom,
  98. left: left,
  99. right: right,
  100. child: SingleChildScrollView(
  101. scrollDirection: Axis.horizontal,
  102. child: InlineActionsHandler(
  103. service: service,
  104. results: initialResults,
  105. editorState: editorState,
  106. menuService: this,
  107. onDismiss: dismiss,
  108. onSelectionUpdate: _onSelectionUpdate,
  109. style: style,
  110. ),
  111. ),
  112. ),
  113. ],
  114. ),
  115. ),
  116. ),
  117. );
  118. Overlay.of(context).insert(_menuEntry!);
  119. editorState.service.keyboardService?.disable(showCursor: true);
  120. editorState.service.scrollService?.disable();
  121. selectionService.currentSelection.addListener(_onSelectionChange);
  122. }
  123. void _onSelectionChange() {
  124. // workaround: SelectionService has been released after hot reload.
  125. final isSelectionDisposed =
  126. editorState.service.selectionServiceKey.currentState == null;
  127. if (!isSelectionDisposed) {
  128. final selectionService = editorState.service.selectionService;
  129. if (selectionService.currentSelection.value == null) {
  130. return;
  131. }
  132. }
  133. if (!selectionChangedByMenu) {
  134. return dismiss();
  135. }
  136. selectionChangedByMenu = false;
  137. }
  138. (double? left, double? top, double? right, double? bottom) _getPosition(
  139. Alignment alignment,
  140. Offset offset,
  141. ) {
  142. double? left, top, right, bottom;
  143. switch (alignment) {
  144. case Alignment.topLeft:
  145. left = offset.dx;
  146. top = offset.dy;
  147. break;
  148. case Alignment.bottomLeft:
  149. left = offset.dx;
  150. bottom = offset.dy;
  151. break;
  152. case Alignment.topRight:
  153. right = offset.dx;
  154. top = offset.dy;
  155. break;
  156. case Alignment.bottomRight:
  157. right = offset.dx;
  158. bottom = offset.dy;
  159. break;
  160. }
  161. return (left, top, right, bottom);
  162. }
  163. }
  164. class InlineActionsMenuStyle {
  165. InlineActionsMenuStyle({
  166. required this.backgroundColor,
  167. required this.groupTextColor,
  168. required this.menuItemTextColor,
  169. required this.menuItemSelectedColor,
  170. required this.menuItemSelectedTextColor,
  171. });
  172. const InlineActionsMenuStyle.light()
  173. : backgroundColor = Colors.white,
  174. groupTextColor = const Color(0xFF555555),
  175. menuItemTextColor = const Color(0xFF333333),
  176. menuItemSelectedColor = const Color(0xFFE0F8FF),
  177. menuItemSelectedTextColor = const Color.fromARGB(255, 56, 91, 247);
  178. const InlineActionsMenuStyle.dark()
  179. : backgroundColor = const Color(0xFF282E3A),
  180. groupTextColor = const Color(0xFFBBC3CD),
  181. menuItemTextColor = const Color(0xFFBBC3CD),
  182. menuItemSelectedColor = const Color(0xFF00BCF0),
  183. menuItemSelectedTextColor = const Color(0xFF131720);
  184. /// The background color of the context menu itself
  185. ///
  186. final Color backgroundColor;
  187. /// The color of the [InlineActionsGroup]'s title text
  188. ///
  189. final Color groupTextColor;
  190. /// The text color of an [InlineActionsMenuItem]
  191. ///
  192. final Color menuItemTextColor;
  193. /// The background of the currently selected [InlineActionsMenuItem]
  194. ///
  195. final Color menuItemSelectedColor;
  196. /// The text color of the currently selected [InlineActionsMenuItem]
  197. ///
  198. final Color menuItemSelectedTextColor;
  199. }