فهرست منبع

Merge pull request #709 from LucasXu0/feat/flowy_editor

feat: Implement arrow up/down/left/right event handler. #708
Nathan.fooo 2 سال پیش
والد
کامیت
b967453047
16فایلهای تغییر یافته به همراه355 افزوده شده و 32 حذف شده
  1. 26 0
      frontend/app_flowy/packages/flowy_editor/example/lib/main.dart
  2. 11 1
      frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart
  3. 31 1
      frontend/app_flowy/packages/flowy_editor/example/lib/plugin/selected_text_node_widget.dart
  4. 5 3
      frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart
  5. 4 4
      frontend/app_flowy/packages/flowy_editor/lib/render/selection/cursor_widget.dart
  6. 58 0
      frontend/app_flowy/packages/flowy_editor/lib/render/selection/floating_shortcut_widget.dart
  7. 4 4
      frontend/app_flowy/packages/flowy_editor/lib/render/selection/flowy_selection_widget.dart
  8. 28 6
      frontend/app_flowy/packages/flowy_editor/lib/render/selection/selectable.dart
  9. 17 2
      frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart
  10. 60 0
      frontend/app_flowy/packages/flowy_editor/lib/service/floating_shortcut_service.dart
  11. 37 0
      frontend/app_flowy/packages/flowy_editor/lib/service/flowy_key_event_handlers/arrow_keys_handler.dart
  12. 3 7
      frontend/app_flowy/packages/flowy_editor/lib/service/flowy_key_event_handlers/delete_single_text_node_handler.dart
  13. 30 0
      frontend/app_flowy/packages/flowy_editor/lib/service/flowy_key_event_handlers/shortcut_handler.dart
  14. 1 1
      frontend/app_flowy/packages/flowy_editor/lib/service/keyboard_service.dart
  15. 13 3
      frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart
  16. 27 0
      frontend/app_flowy/packages/flowy_editor/lib/service/service.dart

+ 26 - 0
frontend/app_flowy/packages/flowy_editor/example/lib/main.dart

@@ -97,6 +97,32 @@ class _MyHomePageState extends State<MyHomePage> {
             return FlowyEditor(
               editorState: _editorState,
               keyEventHandler: const [],
+              shortcuts: [
+                // TODO: this won't work, just a example for now.
+                {
+                  'h1': (editorState, eventName) {
+                    debugPrint('shortcut => $eventName');
+                    final selectedNodes = editorState.selectedNodes;
+                    if (selectedNodes.isEmpty) {
+                      return;
+                    }
+                    final textNode = selectedNodes.first as TextNode;
+                    TransactionBuilder(editorState)
+                      ..formatText(textNode, 0, textNode.toRawString().length, {
+                        'heading': 'h1',
+                      })
+                      ..commit();
+                  }
+                },
+                {
+                  'bold': (editorState, eventName) =>
+                      debugPrint('shortcut => $eventName')
+                },
+                {
+                  'underline': (editorState, eventName) =>
+                      debugPrint('shortcut => $eventName')
+                },
+              ],
             );
           }
         },

+ 11 - 1
frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart

@@ -52,7 +52,7 @@ class __ImageNodeWidgetState extends State<_ImageNodeWidget> with Selectable {
   }
 
   @override
-  TextSelection? getTextSelection() {
+  TextSelection? getCurrentTextSelection() {
     return null;
   }
 
@@ -61,6 +61,16 @@ class __ImageNodeWidgetState extends State<_ImageNodeWidget> with Selectable {
     return Offset.zero;
   }
 
+  @override
+  Offset getBackwardOffset() {
+    return Offset.zero;
+  }
+
+  @override
+  Offset getForwardOffset() {
+    return Offset.zero;
+  }
+
   @override
   Widget build(BuildContext context) {
     return _build(context);

+ 31 - 1
frontend/app_flowy/packages/flowy_editor/example/lib/plugin/selected_text_node_widget.dart

@@ -1,3 +1,5 @@
+import 'dart:math';
+
 import 'package:example/plugin/debuggable_rich_text.dart';
 import 'package:flowy_editor/flowy_editor.dart';
 import 'package:flutter/foundation.dart';
@@ -98,7 +100,7 @@ class _SelectedTextNodeWidgetState extends State<_SelectedTextNodeWidget>
   }
 
   @override
-  TextSelection? getTextSelection() {
+  TextSelection? getCurrentTextSelection() {
     return _textSelection;
   }
 
@@ -108,6 +110,30 @@ class _SelectedTextNodeWidgetState extends State<_SelectedTextNodeWidget>
     return _renderParagraph.localToGlobal(offset);
   }
 
+  @override
+  Offset getBackwardOffset() {
+    final textSelection = _textSelection;
+    if (textSelection != null) {
+      final leftTextSelection = TextSelection.collapsed(
+        offset: max(0, textSelection.baseOffset - 1),
+      );
+      return getOffsetByTextSelection(leftTextSelection);
+    }
+    return Offset.zero;
+  }
+
+  @override
+  Offset getForwardOffset() {
+    final textSelection = _textSelection;
+    if (textSelection != null) {
+      final leftTextSelection = TextSelection.collapsed(
+        offset: min(node.toRawString().length, textSelection.extentOffset + 1),
+      );
+      return getOffsetByTextSelection(leftTextSelection);
+    }
+    return Offset.zero;
+  }
+
   @override
   Widget build(BuildContext context) {
     Widget richText;
@@ -117,6 +143,10 @@ class _SelectedTextNodeWidgetState extends State<_SelectedTextNodeWidget>
       richText = RichText(key: _textKey, text: node.toTextSpan());
     }
 
+    if (node.children.isEmpty) {
+      return richText;
+    }
+
     return Column(
       crossAxisAlignment: CrossAxisAlignment.start,
       children: [

+ 5 - 3
frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart

@@ -1,4 +1,5 @@
 import 'dart:async';
+import 'package:flowy_editor/service/service.dart';
 import 'package:flutter/material.dart';
 
 import 'package:flowy_editor/document/node.dart';
@@ -21,13 +22,14 @@ class ApplyOptions {
   });
 }
 
-// TODO
-final selectionServiceKey = GlobalKey();
-
 class EditorState {
   final StateTree document;
   final RenderPlugins renderPlugins;
   List<Node> selectedNodes = [];
+
+  // Service reference.
+  final service = FlowyService();
+
   final UndoManager undoManager = UndoManager();
   Selection? cursorSelection;
 

+ 4 - 4
frontend/app_flowy/packages/flowy_editor/lib/render/selection/flowy_cursor_widget.dart → frontend/app_flowy/packages/flowy_editor/lib/render/selection/cursor_widget.dart

@@ -2,8 +2,8 @@ import 'dart:async';
 
 import 'package:flutter/material.dart';
 
-class FlowyCursorWidget extends StatefulWidget {
-  const FlowyCursorWidget({
+class CursorWidget extends StatefulWidget {
+  const CursorWidget({
     Key? key,
     required this.layerLink,
     required this.rect,
@@ -17,10 +17,10 @@ class FlowyCursorWidget extends StatefulWidget {
   final LayerLink layerLink;
 
   @override
-  State<FlowyCursorWidget> createState() => _FlowyCursorWidgetState();
+  State<CursorWidget> createState() => _CursorWidgetState();
 }
 
-class _FlowyCursorWidgetState extends State<FlowyCursorWidget> {
+class _CursorWidgetState extends State<CursorWidget> {
   bool showCursor = true;
   late Timer timer;
 

+ 58 - 0
frontend/app_flowy/packages/flowy_editor/lib/render/selection/floating_shortcut_widget.dart

@@ -0,0 +1,58 @@
+import 'package:flowy_editor/flowy_editor.dart';
+import 'package:flutter/material.dart';
+
+typedef FloatingShortcutHandler = void Function(
+    EditorState editorState, String eventName);
+typedef FloatingShortcuts = List<Map<String, FloatingShortcutHandler>>;
+
+class FloatingShortcutWidget extends StatelessWidget {
+  const FloatingShortcutWidget({
+    Key? key,
+    required this.editorState,
+    required this.layerLink,
+    required this.rect,
+    required this.floatingShortcuts,
+  }) : super(key: key);
+
+  final EditorState editorState;
+  final LayerLink layerLink;
+  final Rect rect;
+  final FloatingShortcuts floatingShortcuts;
+
+  List<String> get _shortcutNames =>
+      floatingShortcuts.map((shortcut) => shortcut.keys.first).toList();
+  List<FloatingShortcutHandler> get _shortcutHandlers =>
+      floatingShortcuts.map((shortcut) => shortcut.values.first).toList();
+
+  @override
+  Widget build(BuildContext context) {
+    return Positioned.fromRect(
+      rect: rect,
+      child: CompositedTransformFollower(
+        link: layerLink,
+        offset: rect.topLeft,
+        showWhenUnlinked: true,
+        child: Container(
+          color: Colors.white,
+          child: ListView.builder(
+            itemCount: floatingShortcuts.length,
+            itemBuilder: ((context, index) {
+              final name = _shortcutNameInIndex(index);
+              final handler = _shortcutHandlerInIndex(index);
+              return Card(
+                child: GestureDetector(
+                  onTap: () => handler(editorState, name),
+                  child: ListTile(title: Text(name)),
+                ),
+              );
+            }),
+          ),
+        ),
+      ),
+    );
+  }
+
+  String _shortcutNameInIndex(int index) => _shortcutNames[index];
+  FloatingShortcutHandler _shortcutHandlerInIndex(int index) =>
+      _shortcutHandlers[index];
+}

+ 4 - 4
frontend/app_flowy/packages/flowy_editor/lib/render/selection/flowy_selection_widget.dart

@@ -1,7 +1,7 @@
 import 'package:flutter/material.dart';
 
-class FlowySelectionWidget extends StatefulWidget {
-  const FlowySelectionWidget({
+class SelectionWidget extends StatefulWidget {
+  const SelectionWidget({
     Key? key,
     required this.layerLink,
     required this.rect,
@@ -13,10 +13,10 @@ class FlowySelectionWidget extends StatefulWidget {
   final LayerLink layerLink;
 
   @override
-  State<FlowySelectionWidget> createState() => _FlowySelectionWidgetState();
+  State<SelectionWidget> createState() => _SelectionWidgetState();
 }
 
-class _FlowySelectionWidgetState extends State<FlowySelectionWidget> {
+class _SelectionWidgetState extends State<SelectionWidget> {
   @override
   Widget build(BuildContext context) {
     return Positioned.fromRect(

+ 28 - 6
frontend/app_flowy/packages/flowy_editor/lib/render/selection/selectable.dart

@@ -2,18 +2,40 @@ import 'package:flutter/material.dart';
 
 ///
 mixin Selectable<T extends StatefulWidget> on State<T> {
-  /// Returns a [Rect] list for overlay.
-  /// [start] and [end] are global offsets.
-  /// The return result must be an local offset.
+  /// Returns a [List] of the [Rect] selection sorrounded by start and end
+  ///   in current widget.
+  ///
+  /// [start] and [end] are the offsets under the global coordinate system.
+  ///
+  /// The return result must be a [List] of the [Rect]
+  ///   under the local coordinate system.
   List<Rect> getSelectionRectsInRange(Offset start, Offset end);
 
-  /// Returns a [Rect] for cursor.
-  /// The return result must be an local offset.
+  /// Returns a [Rect] for the offset in current widget.
+  ///
+  /// [start] is the offset of the global coordination system.
+  ///
+  /// The return result must be an offset of the local coordinate system.
   Rect getCursorRect(Offset start);
 
+  /// Returns a backward offset of the current offset based on the cause.
+  Offset getBackwardOffset(/* Cause */);
+
+  /// Returns a forward offset of the current offset based on the cause.
+  Offset getForwardOffset(/* Cause */);
+
   /// For [TextNode] only.
-  TextSelection? getTextSelection();
+  ///
+  /// Returns a [TextSelection] or [Null].
+  ///
+  /// Only the widget rendered by [TextNode] need to implement the detail,
+  ///   and the rest can return null.
+  TextSelection? getCurrentTextSelection();
 
   /// For [TextNode] only.
+  ///
+  /// Retruns a [Offset].
+  /// Only the widget rendered by [TextNode] need to implement the detail,
+  ///   and the rest can return [Offset.zero].
   Offset getOffsetByTextSelection(TextSelection textSelection);
 }

+ 17 - 2
frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart

@@ -1,5 +1,9 @@
+import 'package:flowy_editor/render/selection/floating_shortcut_widget.dart';
+import 'package:flowy_editor/service/floating_shortcut_service.dart';
+import 'package:flowy_editor/service/flowy_key_event_handlers/arrow_keys_handler.dart';
 import 'package:flowy_editor/service/flowy_key_event_handlers/delete_nodes_handler.dart';
 import 'package:flowy_editor/service/flowy_key_event_handlers/delete_single_text_node_handler.dart';
+import 'package:flowy_editor/service/flowy_key_event_handlers/shortcut_handler.dart';
 import 'package:flowy_editor/service/keyboard_service.dart';
 import 'package:flowy_editor/service/selection_service.dart';
 
@@ -11,10 +15,12 @@ class FlowyEditor extends StatefulWidget {
     Key? key,
     required this.editorState,
     required this.keyEventHandler,
+    required this.shortcuts,
   }) : super(key: key);
 
   final EditorState editorState;
   final List<FlowyKeyEventHandler> keyEventHandler;
+  final FloatingShortcuts shortcuts;
 
   @override
   State<FlowyEditor> createState() => _FlowyEditorState();
@@ -26,16 +32,25 @@ class _FlowyEditorState extends State<FlowyEditor> {
   @override
   Widget build(BuildContext context) {
     return FlowySelection(
-      key: selectionServiceKey,
+      key: editorState.service.selectionServiceKey,
       editorState: editorState,
       child: FlowyKeyboard(
+        key: editorState.service.keyboardServiceKey,
         handlers: [
+          slashShortcutHandler,
           flowyDeleteNodesHandler,
           deleteSingleTextNodeHandler,
+          arrowKeysHandler,
           ...widget.keyEventHandler,
         ],
         editorState: editorState,
-        child: editorState.build(context),
+        child: FloatingShortcut(
+          key: editorState.service.floatingShortcutServiceKey,
+          size: const Size(200, 150), // TODO: support customize size.
+          editorState: editorState,
+          floatingShortcuts: widget.shortcuts,
+          child: editorState.build(context),
+        ),
       ),
     );
   }

+ 60 - 0
frontend/app_flowy/packages/flowy_editor/lib/service/floating_shortcut_service.dart

@@ -0,0 +1,60 @@
+import 'package:flowy_editor/flowy_editor.dart';
+import 'package:flowy_editor/render/selection/floating_shortcut_widget.dart';
+import 'package:flutter/material.dart';
+
+mixin FlowyFloatingShortcutService {
+  /// Show the floating shortcut widget beside the offset.
+  void showInOffset(Offset offset, LayerLink layerLink);
+
+  /// Hide the floating shortcut widget.
+  void hide();
+}
+
+class FloatingShortcut extends StatefulWidget {
+  const FloatingShortcut({
+    Key? key,
+    required this.size,
+    required this.editorState,
+    required this.floatingShortcuts,
+    required this.child,
+  }) : super(key: key);
+
+  final Size size;
+  final EditorState editorState;
+  final Widget child;
+  final FloatingShortcuts floatingShortcuts;
+
+  @override
+  State<FloatingShortcut> createState() => _FloatingShortcutState();
+}
+
+class _FloatingShortcutState extends State<FloatingShortcut>
+    with FlowyFloatingShortcutService {
+  OverlayEntry? _floatintShortcutOverlay;
+
+  @override
+  void showInOffset(Offset offset, LayerLink layerLink) {
+    _floatintShortcutOverlay?.remove();
+    _floatintShortcutOverlay = OverlayEntry(
+      builder: (context) => FloatingShortcutWidget(
+          editorState: widget.editorState,
+          layerLink: layerLink,
+          rect: offset.translate(10, 0) & widget.size,
+          floatingShortcuts: widget.floatingShortcuts),
+    );
+    Overlay.of(context)?.insert(_floatintShortcutOverlay!);
+  }
+
+  @override
+  void hide() {
+    _floatintShortcutOverlay?.remove();
+    _floatintShortcutOverlay = null;
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Container(
+      child: widget.child,
+    );
+  }
+}

+ 37 - 0
frontend/app_flowy/packages/flowy_editor/lib/service/flowy_key_event_handlers/arrow_keys_handler.dart

@@ -0,0 +1,37 @@
+import 'package:flowy_editor/extensions/object_extensions.dart';
+import 'package:flowy_editor/flowy_editor.dart';
+import 'package:flowy_editor/service/keyboard_service.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+
+FlowyKeyEventHandler arrowKeysHandler = (editorState, event) {
+  if (event.logicalKey != LogicalKeyboardKey.arrowUp &&
+      event.logicalKey != LogicalKeyboardKey.arrowDown &&
+      event.logicalKey != LogicalKeyboardKey.arrowLeft &&
+      event.logicalKey != LogicalKeyboardKey.arrowRight) {
+    return KeyEventResult.ignored;
+  }
+
+  // TODO: Up and Down
+
+  // Left and Right
+  final selectedNodes = editorState.selectedNodes;
+  if (selectedNodes.length != 1) {
+    return KeyEventResult.ignored;
+  }
+
+  final node = selectedNodes.first.unwrapOrNull<TextNode>();
+  final selectable = node?.key?.currentState?.unwrapOrNull<Selectable>();
+  Offset? offset;
+  if (event.logicalKey == LogicalKeyboardKey.arrowLeft) {
+    offset = selectable?.getBackwardOffset();
+  } else if (event.logicalKey == LogicalKeyboardKey.arrowRight) {
+    offset = selectable?.getForwardOffset();
+  }
+  final selectionService = editorState.service.selectionService;
+  if (offset != null) {
+    selectionService.updateCursor(offset);
+    return KeyEventResult.handled;
+  }
+  return KeyEventResult.ignored;
+};

+ 3 - 7
frontend/app_flowy/packages/flowy_editor/lib/service/flowy_key_event_handlers/delete_single_text_node_handler.dart

@@ -1,10 +1,8 @@
 import 'package:flowy_editor/document/node.dart';
-import 'package:flowy_editor/editor_state.dart';
 import 'package:flowy_editor/operation/transaction_builder.dart';
 import 'package:flowy_editor/render/selection/selectable.dart';
 import 'package:flowy_editor/service/keyboard_service.dart';
 import 'package:flowy_editor/extensions/object_extensions.dart';
-import 'package:flowy_editor/service/selection_service.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
 
@@ -19,7 +17,7 @@ FlowyKeyEventHandler deleteSingleTextNodeHandler = (editorState, event) {
     final node = selectionNodes.first.unwrapOrNull<TextNode>();
     final selectable = node?.key?.currentState?.unwrapOrNull<Selectable>();
     if (selectable != null) {
-      final textSelection = selectable.getTextSelection();
+      final textSelection = selectable.getCurrentTextSelection();
       if (textSelection != null) {
         if (textSelection.isCollapsed) {
           /// Three cases:
@@ -33,8 +31,7 @@ FlowyKeyEventHandler deleteSingleTextNodeHandler = (editorState, event) {
               final previous = node!.previous! as TextNode;
               final newTextSelection = TextSelection.collapsed(
                   offset: previous.toRawString().length);
-              final selectionService =
-                  selectionServiceKey.currentState as FlowySelectionService;
+              final selectionService = editorState.service.selectionService;
               final previousSelectable =
                   previous.key?.currentState?.unwrapOrNull<Selectable>();
               final newOfset = previousSelectable
@@ -58,8 +55,7 @@ FlowyKeyEventHandler deleteSingleTextNodeHandler = (editorState, event) {
               ..commit();
             final newTextSelection =
                 TextSelection.collapsed(offset: textSelection.baseOffset - 1);
-            final selectionService =
-                selectionServiceKey.currentState as FlowySelectionService;
+            final selectionService = editorState.service.selectionService;
             final newOfset =
                 selectable.getOffsetByTextSelection(newTextSelection);
             selectionService.updateCursor(newOfset);

+ 30 - 0
frontend/app_flowy/packages/flowy_editor/lib/service/flowy_key_event_handlers/shortcut_handler.dart

@@ -0,0 +1,30 @@
+import 'package:flowy_editor/flowy_editor.dart';
+import 'package:flowy_editor/service/keyboard_service.dart';
+import 'package:flowy_editor/extensions/object_extensions.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+
+/// type '/' to trigger shortcut widget
+FlowyKeyEventHandler slashShortcutHandler = (editorState, event) {
+  if (event.logicalKey != LogicalKeyboardKey.slash) {
+    return KeyEventResult.ignored;
+  }
+
+  final selectedNodes = editorState.selectedNodes;
+  if (selectedNodes.length != 1) {
+    return KeyEventResult.ignored;
+  }
+
+  final textNode = selectedNodes.first.unwrapOrNull<TextNode>();
+  final selectable = textNode?.key?.currentState?.unwrapOrNull<Selectable>();
+  final textSelection = selectable?.getCurrentTextSelection();
+  if (textNode != null && selectable != null && textSelection != null) {
+    final offset = selectable.getOffsetByTextSelection(textSelection);
+    final rect = selectable.getCursorRect(offset);
+    editorState.service.floatingToolbarService
+        .showInOffset(rect.topLeft, textNode.layerLink);
+    return KeyEventResult.handled;
+  }
+
+  return KeyEventResult.ignored;
+};

+ 1 - 1
frontend/app_flowy/packages/flowy_editor/lib/service/keyboard_service.dart

@@ -46,7 +46,7 @@ class _FlowyKeyboardState extends State<FlowyKeyboard> {
     }
 
     for (final handler in widget.handlers) {
-      debugPrint('handle keyboard event $event by $handler');
+      // debugPrint('handle keyboard event $event by $handler');
 
       KeyEventResult result = handler(widget.editorState, event);
 

+ 13 - 3
frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart

@@ -1,5 +1,7 @@
-import 'package:flowy_editor/render/selection/flowy_cursor_widget.dart';
+import 'package:flowy_editor/render/selection/cursor_widget.dart';
 import 'package:flowy_editor/render/selection/flowy_selection_widget.dart';
+import 'package:flowy_editor/extensions/object_extensions.dart';
+import 'package:flowy_editor/service/floating_shortcut_service.dart';
 import 'package:flutter/gestures.dart';
 import 'package:flutter/material.dart';
 
@@ -120,7 +122,7 @@ class _FlowySelectionState extends State<FlowySelection>
       final selectionRects = selectable.getSelectionRectsInRange(start, end);
       for (final rect in selectionRects) {
         final overlay = OverlayEntry(
-          builder: ((context) => FlowySelectionWidget(
+          builder: ((context) => SelectionWidget(
                 color: Colors.yellow.withAlpha(100),
                 layerLink: node.layerLink,
                 rect: rect,
@@ -149,7 +151,7 @@ class _FlowySelectionState extends State<FlowySelection>
     final selectable = selectedNode.key?.currentState as Selectable;
     final rect = selectable.getCursorRect(start);
     final cursor = OverlayEntry(
-      builder: ((context) => FlowyCursorWidget(
+      builder: ((context) => CursorWidget(
             key: _cursorKey,
             rect: rect,
             color: Colors.red,
@@ -275,6 +277,7 @@ class _FlowySelectionState extends State<FlowySelection>
   void _clearAllOverlayEntries() {
     _clearSelection();
     _clearCursor();
+    _clearFloatingShorts();
   }
 
   void _clearSelection() {
@@ -288,4 +291,11 @@ class _FlowySelectionState extends State<FlowySelection>
       ..forEach((overlay) => overlay.remove())
       ..clear();
   }
+
+  void _clearFloatingShorts() {
+    final shortcutService = editorState
+        .service.floatingShortcutServiceKey.currentState
+        ?.unwrapOrNull<FlowyFloatingShortcutService>();
+    shortcutService?.hide();
+  }
 }

+ 27 - 0
frontend/app_flowy/packages/flowy_editor/lib/service/service.dart

@@ -0,0 +1,27 @@
+import 'package:flowy_editor/service/floating_shortcut_service.dart';
+import 'package:flowy_editor/service/selection_service.dart';
+import 'package:flutter/material.dart';
+
+class FlowyService {
+  // selection service
+  final selectionServiceKey = GlobalKey(debugLabel: 'flowy_selection_service');
+  FlowySelectionService get selectionService {
+    assert(selectionServiceKey.currentState != null &&
+        selectionServiceKey.currentState is FlowySelectionService);
+    return selectionServiceKey.currentState! as FlowySelectionService;
+  }
+
+  // keyboard service
+  final keyboardServiceKey = GlobalKey(debugLabel: 'flowy_keyboard_service');
+
+  // floating shortcut service
+  final floatingShortcutServiceKey =
+      GlobalKey(debugLabel: 'flowy_floating_shortcut_service');
+  FlowyFloatingShortcutService get floatingToolbarService {
+    assert(floatingShortcutServiceKey.currentState != null &&
+        floatingShortcutServiceKey.currentState
+            is FlowyFloatingShortcutService);
+    return floatingShortcutServiceKey.currentState!
+        as FlowyFloatingShortcutService;
+  }
+}