浏览代码

feat: implement floating shortcut

Lucas.Xu 2 年之前
父节点
当前提交
0bf1e61d55

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

@@ -97,6 +97,21 @@ class _MyHomePageState extends State<MyHomePage> {
             return FlowyEditor(
               editorState: _editorState,
               keyEventHandler: const [],
+              shortCuts: [
+                // TODO: this won't work, just a example for now.
+                {
+                  'heading': (editorState, eventName) =>
+                      debugPrint('shortcut => $eventName')
+                },
+                {
+                  'bold': (editorState, eventName) =>
+                      debugPrint('shortcut => $eventName')
+                },
+                {
+                  'underline': (editorState, eventName) =>
+                      debugPrint('shortcut => $eventName')
+                },
+              ],
             );
           }
         },

+ 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 → frontend/app_flowy/packages/flowy_editor/lib/render/selection/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(

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

@@ -1,6 +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';
 
@@ -12,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();
@@ -32,13 +37,20 @@ class _FlowyEditorState extends State<FlowyEditor> {
       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),
+        ),
       ),
     );
   }

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

@@ -0,0 +1,58 @@
+import 'package:flowy_editor/document/node.dart';
+import 'package:flowy_editor/flowy_editor.dart';
+import 'package:flowy_editor/render/selection/floating_shortcut_widget.dart';
+import 'package:flutter/material.dart';
+
+mixin FlowyFloatingShortCutService {
+  void showInOffset(Offset offset, LayerLink layerLink);
+  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,
+    );
+  }
+}

+ 0 - 2
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';
 

+ 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?.getTextSelection();
+  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;
+};

+ 14 - 4
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/flowy_selection_widget.dart';
+import 'package:flowy_editor/render/selection/cursor_widget.dart';
+import 'package:flowy_editor/render/selection/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();
+  }
 }

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

@@ -1,3 +1,4 @@
+import 'package:flowy_editor/service/floating_shortcut_service.dart';
 import 'package:flowy_editor/service/selection_service.dart';
 import 'package:flutter/material.dart';
 
@@ -12,4 +13,15 @@ class FlowyService {
 
   // keyboard service
   final keyboardServiceKey = GlobalKey(debugLabel: 'flowy_keyboard_service');
+
+  // floating toolbar 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;
+  }
 }