From 2f86cac8af9343b3812bfca42f6edfbe003c20a7 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Mon, 25 Jul 2022 15:58:57 +0800 Subject: [PATCH 1/3] feat: Implement arrow up/down/left/right event handler. #708 --- .../example/lib/plugin/image_node_widget.dart | 10 +++++ .../lib/plugin/selected_text_node_widget.dart | 30 +++++++++++++++ .../flowy_editor/lib/editor_state.dart | 8 ++-- .../lib/render/selection/selectable.dart | 6 +++ .../lib/service/editor_service.dart | 5 ++- .../arrow_keys_handler.dart | 37 +++++++++++++++++++ .../delete_single_text_node_handler.dart | 6 +-- .../lib/service/keyboard_service.dart | 2 +- .../flowy_editor/lib/service/service.dart | 15 ++++++++ 9 files changed, 110 insertions(+), 9 deletions(-) create mode 100644 frontend/app_flowy/packages/flowy_editor/lib/service/flowy_key_event_handlers/arrow_keys_handler.dart create mode 100644 frontend/app_flowy/packages/flowy_editor/lib/service/service.dart diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart index 389bfed320..193cc879c9 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart @@ -61,6 +61,16 @@ class __ImageNodeWidgetState extends State<_ImageNodeWidget> with Selectable { return Offset.zero; } + @override + Offset getLeftOfOffset() { + return Offset.zero; + } + + @override + Offset getRightOfOffset() { + return Offset.zero; + } + @override Widget build(BuildContext context) { return _build(context); diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/selected_text_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/selected_text_node_widget.dart index 1124ec3cbb..d827fdfde8 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/selected_text_node_widget.dart +++ b/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'; @@ -108,6 +110,30 @@ class _SelectedTextNodeWidgetState extends State<_SelectedTextNodeWidget> return _renderParagraph.localToGlobal(offset); } + @override + Offset getLeftOfOffset() { + final textSelection = _textSelection; + if (textSelection != null) { + final leftTextSelection = TextSelection.collapsed( + offset: max(0, textSelection.baseOffset - 1), + ); + return getOffsetByTextSelection(leftTextSelection); + } + return Offset.zero; + } + + @override + Offset getRightOfOffset() { + 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: [ diff --git a/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart b/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart index 04a5721ed9..000badf02a 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart +++ b/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'; @@ -19,13 +20,14 @@ class ApplyOptions { }); } -// TODO -final selectionServiceKey = GlobalKey(); - class EditorState { final StateTree document; final RenderPlugins renderPlugins; List selectedNodes = []; + + // Service reference. + final service = FlowyService(); + final UndoManager undoManager = UndoManager(); Selection? cursorSelection; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/selection/selectable.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/selection/selectable.dart index 59849c1a6a..098c246569 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/selection/selectable.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/selection/selectable.dart @@ -11,6 +11,12 @@ mixin Selectable on State { /// The return result must be an local offset. Rect getCursorRect(Offset start); + /// Returns one unit offset to the left of the offset + Offset getLeftOfOffset(/* Cause */); + + /// Returns one unit offset to the right of the offset + Offset getRightOfOffset(/* Cause */); + /// For [TextNode] only. TextSelection? getTextSelection(); diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart index d0efac2a0f..2ebb9ce14a 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart @@ -1,3 +1,4 @@ +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/keyboard_service.dart'; @@ -26,12 +27,14 @@ class _FlowyEditorState extends State { @override Widget build(BuildContext context) { return FlowySelection( - key: selectionServiceKey, + key: editorState.service.selectionServiceKey, editorState: editorState, child: FlowyKeyboard( + key: editorState.service.keyboardServiceKey, handlers: [ flowyDeleteNodesHandler, deleteSingleTextNodeHandler, + arrowKeysHandler, ...widget.keyEventHandler, ], editorState: editorState, diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/flowy_key_event_handlers/arrow_keys_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/flowy_key_event_handlers/arrow_keys_handler.dart new file mode 100644 index 0000000000..4de5b61968 --- /dev/null +++ b/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(); + final selectable = node?.key?.currentState?.unwrapOrNull(); + Offset? offset; + if (event.logicalKey == LogicalKeyboardKey.arrowLeft) { + offset = selectable?.getLeftOfOffset(); + } else if (event.logicalKey == LogicalKeyboardKey.arrowRight) { + offset = selectable?.getRightOfOffset(); + } + final selectionService = editorState.service.selectionService; + if (offset != null) { + selectionService.updateCursor(offset); + return KeyEventResult.handled; + } + return KeyEventResult.ignored; +}; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/flowy_key_event_handlers/delete_single_text_node_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/flowy_key_event_handlers/delete_single_text_node_handler.dart index 3c1c1c9e95..5affb8800f 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/flowy_key_event_handlers/delete_single_text_node_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/flowy_key_event_handlers/delete_single_text_node_handler.dart @@ -33,8 +33,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(); final newOfset = previousSelectable @@ -58,8 +57,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); diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/keyboard_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/keyboard_service.dart index 060a9c98fb..ebd66894a7 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/keyboard_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/keyboard_service.dart @@ -46,7 +46,7 @@ class _FlowyKeyboardState extends State { } 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); diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/service.dart new file mode 100644 index 0000000000..8c436cee7f --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/service.dart @@ -0,0 +1,15 @@ +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'); +} From 0bf1e61d5503be09463e5cab7fed892b858e8e0d Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Mon, 25 Jul 2022 18:28:00 +0800 Subject: [PATCH 2/3] feat: implement floating shortcut --- .../flowy_editor/example/lib/main.dart | 15 +++++ ..._cursor_widget.dart => cursor_widget.dart} | 8 +-- .../selection/floating_shortcut_widget.dart | 58 +++++++++++++++++++ ...tion_widget.dart => selection_widget.dart} | 8 +-- .../lib/service/editor_service.dart | 14 ++++- .../service/floating_shortcut_service.dart | 58 +++++++++++++++++++ .../delete_single_text_node_handler.dart | 2 - .../shortcut_handler.dart | 30 ++++++++++ .../lib/service/selection_service.dart | 18 ++++-- .../flowy_editor/lib/service/service.dart | 12 ++++ 10 files changed, 208 insertions(+), 15 deletions(-) rename frontend/app_flowy/packages/flowy_editor/lib/render/selection/{flowy_cursor_widget.dart => cursor_widget.dart} (83%) create mode 100644 frontend/app_flowy/packages/flowy_editor/lib/render/selection/floating_shortcut_widget.dart rename frontend/app_flowy/packages/flowy_editor/lib/render/selection/{flowy_selection_widget.dart => selection_widget.dart} (71%) create mode 100644 frontend/app_flowy/packages/flowy_editor/lib/service/floating_shortcut_service.dart create mode 100644 frontend/app_flowy/packages/flowy_editor/lib/service/flowy_key_event_handlers/shortcut_handler.dart diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart index 83960275e6..6764c2faf1 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart @@ -97,6 +97,21 @@ class _MyHomePageState extends State { 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') + }, + ], ); } }, diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/selection/flowy_cursor_widget.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/selection/cursor_widget.dart similarity index 83% rename from frontend/app_flowy/packages/flowy_editor/lib/render/selection/flowy_cursor_widget.dart rename to frontend/app_flowy/packages/flowy_editor/lib/render/selection/cursor_widget.dart index 9ab61e5c47..2ba42221f0 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/selection/flowy_cursor_widget.dart +++ b/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 createState() => _FlowyCursorWidgetState(); + State createState() => _CursorWidgetState(); } -class _FlowyCursorWidgetState extends State { +class _CursorWidgetState extends State { bool showCursor = true; late Timer timer; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/selection/floating_shortcut_widget.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/selection/floating_shortcut_widget.dart new file mode 100644 index 0000000000..b91ed19fe2 --- /dev/null +++ b/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>; + +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 get _shortcutNames => + floatingShortcuts.map((shortcut) => shortcut.keys.first).toList(); + List 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]; +} diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/selection/flowy_selection_widget.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/selection/selection_widget.dart similarity index 71% rename from frontend/app_flowy/packages/flowy_editor/lib/render/selection/flowy_selection_widget.dart rename to frontend/app_flowy/packages/flowy_editor/lib/render/selection/selection_widget.dart index f3def681e1..96dd6a7759 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/selection/flowy_selection_widget.dart +++ b/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 createState() => _FlowySelectionWidgetState(); + State createState() => _SelectionWidgetState(); } -class _FlowySelectionWidgetState extends State { +class _SelectionWidgetState extends State { @override Widget build(BuildContext context) { return Positioned.fromRect( diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart index 2ebb9ce14a..0571cf4e03 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart +++ b/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 keyEventHandler; + final FloatingShortCuts shortCuts; @override State createState() => _FlowyEditorState(); @@ -32,13 +37,20 @@ class _FlowyEditorState extends State { 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), + ), ), ); } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/floating_shortcut_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/floating_shortcut_service.dart new file mode 100644 index 0000000000..ed1a6a4528 --- /dev/null +++ b/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 createState() => _FloatingShortCutState(); +} + +class _FloatingShortCutState extends State + 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, + ); + } +} diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/flowy_key_event_handlers/delete_single_text_node_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/flowy_key_event_handlers/delete_single_text_node_handler.dart index 5affb8800f..1358276f47 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/flowy_key_event_handlers/delete_single_text_node_handler.dart +++ b/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'; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/flowy_key_event_handlers/shortcut_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/flowy_key_event_handlers/shortcut_handler.dart new file mode 100644 index 0000000000..074e021f79 --- /dev/null +++ b/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(); + final selectable = textNode?.key?.currentState?.unwrapOrNull(); + 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; +}; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart index 99b0efb467..778e657340 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart +++ b/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 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 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 void _clearAllOverlayEntries() { _clearSelection(); _clearCursor(); + _clearFloatingShorts(); } void _clearSelection() { @@ -288,4 +291,11 @@ class _FlowySelectionState extends State ..forEach((overlay) => overlay.remove()) ..clear(); } + + void _clearFloatingShorts() { + final shortCutService = editorState + .service.floatingShortcutServiceKey.currentState + ?.unwrapOrNull(); + shortCutService?.hide(); + } } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/service.dart index 8c436cee7f..8ade6d26be 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/service.dart +++ b/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; + } } From fcb09e96367259d9a1951dad97dcc1141b3e3b9d Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Mon, 25 Jul 2022 20:18:14 +0800 Subject: [PATCH 3/3] feat: typo and document --- .../flowy_editor/example/lib/main.dart | 17 +++++++-- .../example/lib/plugin/image_node_widget.dart | 6 ++-- .../lib/plugin/selected_text_node_widget.dart | 6 ++-- .../selection/floating_shortcut_widget.dart | 12 +++---- ...idget.dart => flowy_selection_widget.dart} | 0 .../lib/render/selection/selectable.dart | 36 +++++++++++++------ .../lib/service/editor_service.dart | 8 ++--- .../service/floating_shortcut_service.dart | 22 ++++++------ .../arrow_keys_handler.dart | 4 +-- .../delete_single_text_node_handler.dart | 2 +- .../shortcut_handler.dart | 2 +- .../lib/service/selection_service.dart | 8 ++--- .../flowy_editor/lib/service/service.dart | 8 ++--- 13 files changed, 80 insertions(+), 51 deletions(-) rename frontend/app_flowy/packages/flowy_editor/lib/render/selection/{selection_widget.dart => flowy_selection_widget.dart} (100%) diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart index 6764c2faf1..9cc4d8c536 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart @@ -97,11 +97,22 @@ class _MyHomePageState extends State { return FlowyEditor( editorState: _editorState, keyEventHandler: const [], - shortCuts: [ + shortcuts: [ // TODO: this won't work, just a example for now. { - 'heading': (editorState, eventName) => - debugPrint('shortcut => $eventName') + '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) => diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart index 193cc879c9..8a9c96b22e 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart +++ b/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; } @@ -62,12 +62,12 @@ class __ImageNodeWidgetState extends State<_ImageNodeWidget> with Selectable { } @override - Offset getLeftOfOffset() { + Offset getBackwardOffset() { return Offset.zero; } @override - Offset getRightOfOffset() { + Offset getForwardOffset() { return Offset.zero; } diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/selected_text_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/selected_text_node_widget.dart index d827fdfde8..b234ecd967 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/selected_text_node_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/selected_text_node_widget.dart @@ -100,7 +100,7 @@ class _SelectedTextNodeWidgetState extends State<_SelectedTextNodeWidget> } @override - TextSelection? getTextSelection() { + TextSelection? getCurrentTextSelection() { return _textSelection; } @@ -111,7 +111,7 @@ class _SelectedTextNodeWidgetState extends State<_SelectedTextNodeWidget> } @override - Offset getLeftOfOffset() { + Offset getBackwardOffset() { final textSelection = _textSelection; if (textSelection != null) { final leftTextSelection = TextSelection.collapsed( @@ -123,7 +123,7 @@ class _SelectedTextNodeWidgetState extends State<_SelectedTextNodeWidget> } @override - Offset getRightOfOffset() { + Offset getForwardOffset() { final textSelection = _textSelection; if (textSelection != null) { final leftTextSelection = TextSelection.collapsed( diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/selection/floating_shortcut_widget.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/selection/floating_shortcut_widget.dart index b91ed19fe2..9fbbbbcb01 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/selection/floating_shortcut_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/selection/floating_shortcut_widget.dart @@ -1,9 +1,9 @@ import 'package:flowy_editor/flowy_editor.dart'; import 'package:flutter/material.dart'; -typedef FloatingShortCutHandler = void Function( +typedef FloatingShortcutHandler = void Function( EditorState editorState, String eventName); -typedef FloatingShortCuts = List>; +typedef FloatingShortcuts = List>; class FloatingShortcutWidget extends StatelessWidget { const FloatingShortcutWidget({ @@ -17,11 +17,11 @@ class FloatingShortcutWidget extends StatelessWidget { final EditorState editorState; final LayerLink layerLink; final Rect rect; - final FloatingShortCuts floatingShortcuts; + final FloatingShortcuts floatingShortcuts; List get _shortcutNames => floatingShortcuts.map((shortcut) => shortcut.keys.first).toList(); - List get _shortcutHandlers => + List get _shortcutHandlers => floatingShortcuts.map((shortcut) => shortcut.values.first).toList(); @override @@ -38,7 +38,7 @@ class FloatingShortcutWidget extends StatelessWidget { itemCount: floatingShortcuts.length, itemBuilder: ((context, index) { final name = _shortcutNameInIndex(index); - final handler = _shortCutHandlerInIndex(index); + final handler = _shortcutHandlerInIndex(index); return Card( child: GestureDetector( onTap: () => handler(editorState, name), @@ -53,6 +53,6 @@ class FloatingShortcutWidget extends StatelessWidget { } String _shortcutNameInIndex(int index) => _shortcutNames[index]; - FloatingShortCutHandler _shortCutHandlerInIndex(int index) => + FloatingShortcutHandler _shortcutHandlerInIndex(int index) => _shortcutHandlers[index]; } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/selection/selection_widget.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/selection/flowy_selection_widget.dart similarity index 100% rename from frontend/app_flowy/packages/flowy_editor/lib/render/selection/selection_widget.dart rename to frontend/app_flowy/packages/flowy_editor/lib/render/selection/flowy_selection_widget.dart diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/selection/selectable.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/selection/selectable.dart index 098c246569..6fc51049a1 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/selection/selectable.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/selection/selectable.dart @@ -2,24 +2,40 @@ import 'package:flutter/material.dart'; /// mixin Selectable on State { - /// 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 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 one unit offset to the left of the offset - Offset getLeftOfOffset(/* Cause */); + /// Returns a backward offset of the current offset based on the cause. + Offset getBackwardOffset(/* Cause */); - /// Returns one unit offset to the right of the offset - Offset getRightOfOffset(/* 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); } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart index 0571cf4e03..7cd4eaf708 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart @@ -15,12 +15,12 @@ class FlowyEditor extends StatefulWidget { Key? key, required this.editorState, required this.keyEventHandler, - required this.shortCuts, + required this.shortcuts, }) : super(key: key); final EditorState editorState; final List keyEventHandler; - final FloatingShortCuts shortCuts; + final FloatingShortcuts shortcuts; @override State createState() => _FlowyEditorState(); @@ -44,11 +44,11 @@ class _FlowyEditorState extends State { ...widget.keyEventHandler, ], editorState: editorState, - child: FloatingShortCut( + child: FloatingShortcut( key: editorState.service.floatingShortcutServiceKey, size: const Size(200, 150), // TODO: support customize size. editorState: editorState, - floatingShortCuts: widget.shortCuts, + floatingShortcuts: widget.shortcuts, child: editorState.build(context), ), ), diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/floating_shortcut_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/floating_shortcut_service.dart index ed1a6a4528..774d906acc 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/floating_shortcut_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/floating_shortcut_service.dart @@ -1,33 +1,35 @@ -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 { +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({ +class FloatingShortcut extends StatefulWidget { + const FloatingShortcut({ Key? key, required this.size, required this.editorState, - required this.floatingShortCuts, + required this.floatingShortcuts, required this.child, }) : super(key: key); final Size size; final EditorState editorState; final Widget child; - final FloatingShortCuts floatingShortCuts; + final FloatingShortcuts floatingShortcuts; @override - State createState() => _FloatingShortCutState(); + State createState() => _FloatingShortcutState(); } -class _FloatingShortCutState extends State - with FlowyFloatingShortCutService { +class _FloatingShortcutState extends State + with FlowyFloatingShortcutService { OverlayEntry? _floatintShortcutOverlay; @override @@ -38,7 +40,7 @@ class _FloatingShortCutState extends State editorState: widget.editorState, layerLink: layerLink, rect: offset.translate(10, 0) & widget.size, - floatingShortcuts: widget.floatingShortCuts), + floatingShortcuts: widget.floatingShortcuts), ); Overlay.of(context)?.insert(_floatintShortcutOverlay!); } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/flowy_key_event_handlers/arrow_keys_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/flowy_key_event_handlers/arrow_keys_handler.dart index 4de5b61968..3049f54453 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/flowy_key_event_handlers/arrow_keys_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/flowy_key_event_handlers/arrow_keys_handler.dart @@ -24,9 +24,9 @@ FlowyKeyEventHandler arrowKeysHandler = (editorState, event) { final selectable = node?.key?.currentState?.unwrapOrNull(); Offset? offset; if (event.logicalKey == LogicalKeyboardKey.arrowLeft) { - offset = selectable?.getLeftOfOffset(); + offset = selectable?.getBackwardOffset(); } else if (event.logicalKey == LogicalKeyboardKey.arrowRight) { - offset = selectable?.getRightOfOffset(); + offset = selectable?.getForwardOffset(); } final selectionService = editorState.service.selectionService; if (offset != null) { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/flowy_key_event_handlers/delete_single_text_node_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/flowy_key_event_handlers/delete_single_text_node_handler.dart index 1358276f47..db12d2bbb2 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/flowy_key_event_handlers/delete_single_text_node_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/flowy_key_event_handlers/delete_single_text_node_handler.dart @@ -17,7 +17,7 @@ FlowyKeyEventHandler deleteSingleTextNodeHandler = (editorState, event) { final node = selectionNodes.first.unwrapOrNull(); final selectable = node?.key?.currentState?.unwrapOrNull(); if (selectable != null) { - final textSelection = selectable.getTextSelection(); + final textSelection = selectable.getCurrentTextSelection(); if (textSelection != null) { if (textSelection.isCollapsed) { /// Three cases: diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/flowy_key_event_handlers/shortcut_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/flowy_key_event_handlers/shortcut_handler.dart index 074e021f79..4e52d1bbe9 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/flowy_key_event_handlers/shortcut_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/flowy_key_event_handlers/shortcut_handler.dart @@ -17,7 +17,7 @@ FlowyKeyEventHandler slashShortcutHandler = (editorState, event) { final textNode = selectedNodes.first.unwrapOrNull(); final selectable = textNode?.key?.currentState?.unwrapOrNull(); - final textSelection = selectable?.getTextSelection(); + final textSelection = selectable?.getCurrentTextSelection(); if (textNode != null && selectable != null && textSelection != null) { final offset = selectable.getOffsetByTextSelection(textSelection); final rect = selectable.getCursorRect(offset); diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart index 778e657340..2f4bba86ec 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart @@ -1,5 +1,5 @@ import 'package:flowy_editor/render/selection/cursor_widget.dart'; -import 'package:flowy_editor/render/selection/selection_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'; @@ -293,9 +293,9 @@ class _FlowySelectionState extends State } void _clearFloatingShorts() { - final shortCutService = editorState + final shortcutService = editorState .service.floatingShortcutServiceKey.currentState - ?.unwrapOrNull(); - shortCutService?.hide(); + ?.unwrapOrNull(); + shortcutService?.hide(); } } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/service.dart index 8ade6d26be..7833e6d379 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/service.dart @@ -14,14 +14,14 @@ class FlowyService { // keyboard service final keyboardServiceKey = GlobalKey(debugLabel: 'flowy_keyboard_service'); - // floating toolbar service + // floating shortcut service final floatingShortcutServiceKey = GlobalKey(debugLabel: 'flowy_floating_shortcut_service'); - FlowyFloatingShortCutService get floatingToolbarService { + FlowyFloatingShortcutService get floatingToolbarService { assert(floatingShortcutServiceKey.currentState != null && floatingShortcutServiceKey.currentState - is FlowyFloatingShortCutService); + is FlowyFloatingShortcutService); return floatingShortcutServiceKey.currentState! - as FlowyFloatingShortCutService; + as FlowyFloatingShortcutService; } }