From c643c02887d0b28fb634798c865717f550a2c5ac Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Fri, 22 Jul 2022 00:46:25 +0800 Subject: [PATCH] feat: add keyboard example --- .../lib/plugin/document_node_widget.dart | 1 - .../example/lib/plugin/image_node_widget.dart | 5 + .../lib/plugin/selected_text_node_widget.dart | 11 +- .../flowy_editor/lib/editor_state.dart | 149 +----------------- .../lib/flowy_editor_service.dart | 6 +- .../lib/flowy_keyboard_service.dart | 44 +++++- .../lib/flowy_selection_service.dart | 2 + .../packages/flowy_editor/lib/keyboard.dart | 28 ++-- .../flowy_editor/lib/render/selectable.dart | 5 +- 9 files changed, 79 insertions(+), 172 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/document_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/document_node_widget.dart index f9ab3104da..2db1ef89c4 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/document_node_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/document_node_widget.dart @@ -1,5 +1,4 @@ import 'package:flowy_editor/flowy_editor.dart'; -import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; class EditorNodeWidgetBuilder extends NodeWidgetBuilder { 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 d5e68bead2..f1719db744 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 @@ -57,6 +57,11 @@ class __ImageNodeWidgetState extends State<_ImageNodeWidget> return cursorOffset & size; } + @override + TextSelection? getTextSelection() { + return null; + } + @override KeyEventResult onKeyDown(RawKeyEvent event) { if (event.logicalKey == LogicalKeyboardKey.backspace) { 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 7c37d69c73..356a21e4f2 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 @@ -94,6 +94,11 @@ class _SelectedTextNodeWidgetState extends State<_SelectedTextNodeWidget> return _computeCursorRect(textSelection.baseOffset); } + @override + TextSelection? getTextSelection() { + return _textSelection; + } + @override KeyEventResult onKeyDown(RawKeyEvent event) { if (event.logicalKey == LogicalKeyboardKey.backspace) { @@ -111,9 +116,9 @@ class _SelectedTextNodeWidgetState extends State<_SelectedTextNodeWidget> TransactionBuilder(editorState) ..deleteText(node, textSelection.start - 1, 1) ..commit(); - final rect = _computeCursorRect(textSelection.baseOffset - 1); - editorState.tapOffset = rect.center; - editorState.updateCursor(); + // final rect = _computeCursorRect(textSelection.baseOffset - 1); + // editorState.tapOffset = rect.center; + // editorState.updateCursor(); } } else { TransactionBuilder(editorState) 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 20c02a031a..ea9b5bfee1 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart @@ -15,10 +15,7 @@ import './render/render_plugins.dart'; class EditorState { final StateTree document; final RenderPlugins renderPlugins; - - Offset? tapOffset; - Offset? panStartOffset; - Offset? panEndOffset; + List selectedNodes = []; Selection? cursorSelection; @@ -59,148 +56,4 @@ class EditorState { document.textEdit(op.path, op.delta); } } - - List selectionOverlays = []; - - void updateCursor() { - selectionOverlays - ..forEach((element) => element.remove()) - ..clear(); - - if (tapOffset == null) { - return; - } - - // TODO: upward and backward - final selectedNode = _calculateSelectedNode(document.root, tapOffset!); - if (selectedNode.isEmpty) { - return; - } - final key = selectedNode.first.key; - if (key != null && key.currentState is Selectable) { - final selectable = key.currentState as Selectable; - final rect = selectable.getCursorRect(tapOffset!); - final overlay = OverlayEntry(builder: ((context) { - return Positioned.fromRect( - rect: rect, - child: Container( - color: Colors.red, - ), - ); - })); - selectionOverlays.add(overlay); - Overlay.of(selectable.context)?.insert(overlay); - } - } - - void updateSelection() { - selectionOverlays - ..forEach((element) => element.remove()) - ..clear(); - - final selectedNodes = this.selectedNodes; - if (selectedNodes.isEmpty || - panStartOffset == null || - panEndOffset == null) { - return; - } - - for (final node in selectedNodes) { - final key = node.key; - if (key != null && key.currentState is Selectable) { - final selectable = key.currentState as Selectable; - final overlayRects = selectable.getSelectionRectsInSelection( - panStartOffset!, panEndOffset!); - for (final rect in overlayRects) { - // TODO: refactor overlay implement. - final overlay = OverlayEntry(builder: ((context) { - return Positioned.fromRect( - rect: rect, - child: Container( - color: Colors.yellow.withAlpha(100), - ), - ); - })); - selectionOverlays.add(overlay); - Overlay.of(selectable.context)?.insert(overlay); - } - } - } - } - - List get selectedNodes { - if (panStartOffset != null && panEndOffset != null) { - return _calculateSelectedNodes( - document.root, panStartOffset!, panEndOffset!); - } - if (tapOffset != null) { - return _calculateSelectedNode(document.root, tapOffset!); - } - return []; - } - - List _calculateSelectedNode(Node node, Offset offset) { - List result = []; - - /// Skip the node without parent because it is the topmost node. - /// Skip the node without key because it cannot get the [RenderObject]. - if (node.parent != null && node.key != null) { - if (_isNodeInOffset(node, offset)) { - result.add(node); - } - } - - /// - for (final child in node.children) { - result.addAll(_calculateSelectedNode(child, offset)); - } - - return result; - } - - bool _isNodeInOffset(Node node, Offset offset) { - assert(node.key != null); - final renderBox = - node.key?.currentContext?.findRenderObject() as RenderBox?; - if (renderBox == null) { - return false; - } - final boxOffset = renderBox.localToGlobal(Offset.zero); - final boxRect = boxOffset & renderBox.size; - return boxRect.contains(offset); - } - - List _calculateSelectedNodes(Node node, Offset start, Offset end) { - List result = []; - - /// Skip the node without parent because it is the topmost node. - /// Skip the node without key because it cannot get the [RenderObject]. - if (node.parent != null && node.key != null) { - if (_isNodeInRange(node, start, end)) { - result.add(node); - } - } - - /// - for (final child in node.children) { - result.addAll(_calculateSelectedNodes(child, start, end)); - } - - return result; - } - - bool _isNodeInRange(Node node, Offset start, Offset end) { - assert(node.key != null); - final renderBox = - node.key?.currentContext?.findRenderObject() as RenderBox?; - - /// Return false directly if the [RenderBox] cannot found. - if (renderBox == null) { - return false; - } - - final rect = Rect.fromPoints(start, end); - final boxOffset = renderBox.localToGlobal(Offset.zero); - return rect.overlaps(boxOffset & renderBox.size); - } } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor_service.dart index 78dd6809d3..01b4fdf419 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor_service.dart @@ -24,7 +24,11 @@ class _FlowyEditorState extends State { return FlowySelectionWidget( editorState: editorState, child: FlowyKeyboardWidget( - handlers: const [], + handlers: [ + FlowyKeyboradBackSpaceHandler( + editorState: editorState, + ) + ], editorState: editorState, child: editorState.build(context), ), diff --git a/frontend/app_flowy/packages/flowy_editor/lib/flowy_keyboard_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/flowy_keyboard_service.dart index 9593530033..c7752abae5 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/flowy_keyboard_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/flowy_keyboard_service.dart @@ -1,3 +1,7 @@ +import 'package:flowy_editor/document/node.dart'; +import 'package:flowy_editor/operation/transaction.dart'; +import 'package:flowy_editor/operation/transaction_builder.dart'; +import 'package:flowy_editor/render/selectable.dart'; import 'package:flutter/services.dart'; import 'editor_state.dart'; @@ -5,14 +9,44 @@ import 'package:flutter/material.dart'; abstract class FlowyKeyboardHandler { final EditorState editorState; - final RawKeyEvent rawKeyEvent; FlowyKeyboardHandler({ required this.editorState, - required this.rawKeyEvent, }); - KeyEventResult onKeyDown(); + KeyEventResult onKeyDown(RawKeyEvent event); +} + +class FlowyKeyboradBackSpaceHandler extends FlowyKeyboardHandler { + FlowyKeyboradBackSpaceHandler({ + required super.editorState, + }); + + @override + KeyEventResult onKeyDown(RawKeyEvent event) { + final selectedNodes = editorState.selectedNodes; + if (selectedNodes.isNotEmpty) { + // handle delete text + // TODO: type: cursor or selection + if (selectedNodes.length == 1) { + final node = selectedNodes.first; + if (node is TextNode) { + final selectable = node.key?.currentState as Selectable?; + final textSelection = selectable?.getTextSelection(); + if (textSelection != null) { + if (textSelection.isCollapsed) { + TransactionBuilder(editorState) + ..deleteText(node, textSelection.start - 1, 1) + ..commit(); + // TODO: update selection?? + } + } + } + } + return KeyEventResult.handled; + } + return KeyEventResult.ignored; + } } /// Process keyboard events @@ -46,6 +80,8 @@ class _FlowyKeyboardWidgetState extends State { } KeyEventResult _onKey(FocusNode node, RawKeyEvent event) { + debugPrint('on keyboard event $event'); + if (event is! RawKeyDownEvent) { return KeyEventResult.ignored; } @@ -53,7 +89,7 @@ class _FlowyKeyboardWidgetState extends State { for (final handler in widget.handlers) { debugPrint('handle keyboard event $event by $handler'); - KeyEventResult result = handler.onKeyDown(); + KeyEventResult result = handler.onKeyDown(event); switch (result) { case KeyEventResult.handled: diff --git a/frontend/app_flowy/packages/flowy_editor/lib/flowy_selection_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/flowy_selection_service.dart index 77c8474a07..6c55d6f955 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/flowy_selection_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/flowy_selection_service.dart @@ -102,6 +102,7 @@ class _FlowySelectionWidgetState extends State _clearOverlay(); final nodes = selectedNodes; + editorState.selectedNodes = nodes; if (nodes.isEmpty || panStartOffset == null || panEndOffset == null) { assert(panStartOffset == null); assert(panEndOffset == null); @@ -139,6 +140,7 @@ class _FlowySelectionWidgetState extends State } final nodes = selectedNodes; + editorState.selectedNodes = nodes; if (nodes.isEmpty) { return; } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/keyboard.dart b/frontend/app_flowy/packages/flowy_editor/lib/keyboard.dart index 0077b38130..4cb39ce31e 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/keyboard.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/keyboard.dart @@ -26,20 +26,20 @@ class Keyboard extends StatelessWidget { } KeyEventResult _onKey(FocusNode node, RawKeyEvent event) { - if (event is! RawKeyDownEvent) { - return KeyEventResult.ignored; - } - List result = []; - for (final node in editorState.selectedNodes) { - if (node.key != null && - node.key?.currentState is KeyboardEventsRespondable) { - final respondable = node.key!.currentState as KeyboardEventsRespondable; - result.add(respondable.onKeyDown(event)); - } - } - if (result.contains(KeyEventResult.handled)) { - return KeyEventResult.handled; - } + // if (event is! RawKeyDownEvent) { + // return KeyEventResult.ignored; + // } + // List result = []; + // for (final node in editorState.selectedNodes) { + // if (node.key != null && + // node.key?.currentState is KeyboardEventsRespondable) { + // final respondable = node.key!.currentState as KeyboardEventsRespondable; + // result.add(respondable.onKeyDown(event)); + // } + // } + // if (result.contains(KeyEventResult.handled)) { + // return KeyEventResult.handled; + // } return KeyEventResult.ignored; } } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/selectable.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/selectable.dart index f040eee98d..8d1951996d 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/selectable.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/selectable.dart @@ -6,8 +6,11 @@ mixin Selectable on State { /// [start] and [end] are global offsets. List getSelectionRectsInSelection(Offset start, Offset end); - /// Returns a [Rect] for cursor + /// Returns a [Rect] for cursor. Rect getCursorRect(Offset start); + + /// For [TextNode] only. + TextSelection? getTextSelection(); } mixin KeyboardEventsRespondable on State {