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; + } }