From e74f5e84dc0db1293f4a478988a16e589798d0a1 Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Wed, 27 Jul 2022 15:46:43 +0800 Subject: [PATCH 1/6] feat: handle arrow keys --- .../example/lib/plugin/text_node_widget.dart | 2 +- .../flowy_editor/lib/editor_state.dart | 19 +++++- .../arrow_keys_handler.dart | 59 +++++++++++++++++++ .../lib/service/selection_service.dart | 21 +++---- 4 files changed, 88 insertions(+), 13 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart index a67ebcd2ad..53c33cd295 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart @@ -126,7 +126,7 @@ class __TextNodeWidgetState extends State<_TextNodeWidget> textCapitalization: TextCapitalization.sentences, ), ); - editorState.cursorSelection = _localSelectionToGlobal(node, selection); + editorState.updateCursorSelection(_localSelectionToGlobal(node, selection)); _textInputConnection ?..show() ..setEditingState( 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 cd503843c2..a69b053c90 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart @@ -31,7 +31,22 @@ class EditorState { final service = FlowyService(); final UndoManager undoManager = UndoManager(); - Selection? cursorSelection; + Selection? _cursorSelection; + + Selection? get cursorSelection { + return _cursorSelection; + } + + /// add the set reason in the future, don't use setter + updateCursorSelection(Selection? cursorSelection) { + // broadcast to other users here + if (cursorSelection == null) { + service.selectionService.clearSelection(); + } else { + service.selectionService.updateSelection(cursorSelection); + } + _cursorSelection = cursorSelection; + } Timer? _debouncedSealHistoryItemTimer; @@ -58,7 +73,7 @@ class EditorState { for (final op in transaction.operations) { _applyOperation(op); } - cursorSelection = transaction.afterSelection; + updateCursorSelection(transaction.afterSelection); if (options.recordUndo) { final undoItem = undoManager.getUndoHistoryItem(); diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/arrow_keys_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/arrow_keys_handler.dart index 95496db2ea..bdb473042d 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/arrow_keys_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/arrow_keys_handler.dart @@ -1,7 +1,17 @@ +import 'package:flowy_editor/document/node.dart'; +import 'package:flowy_editor/document/position.dart'; import 'package:flowy_editor/service/keyboard_service.dart'; +import 'package:flowy_editor/document/selection.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +int _endOffsetOfNode(Node node) { + if (node is TextNode) { + return node.delta.length; + } + return 0; +} + FlowyKeyEventHandler arrowKeysHandler = (editorState, event) { if (event.logicalKey != LogicalKeyboardKey.arrowUp && event.logicalKey != LogicalKeyboardKey.arrowDown && @@ -10,5 +20,54 @@ FlowyKeyEventHandler arrowKeysHandler = (editorState, event) { return KeyEventResult.ignored; } + final currentSelection = editorState.cursorSelection; + if (currentSelection == null) { + return KeyEventResult.ignored; + } + + if (event.logicalKey == LogicalKeyboardKey.arrowLeft) { + // turn left + if (currentSelection.isCollapsed) { + final end = currentSelection.end; + final offset = end.offset; + if (offset == 0) { + final node = editorState.document.nodeAtPath(end.path)!; + final prevNode = node.previous; + if (prevNode != null) { + editorState.updateCursorSelection(Selection.collapsed(Position( + path: prevNode.path, offset: _endOffsetOfNode(prevNode)))); + } + return KeyEventResult.handled; + } + editorState.updateCursorSelection( + Selection.collapsed(Position(path: end.path, offset: offset - 1))); + } else { + editorState + .updateCursorSelection(currentSelection.collapse(atStart: true)); + } + return KeyEventResult.handled; + } else if (event.logicalKey == LogicalKeyboardKey.arrowRight) { + if (currentSelection.isCollapsed) { + final end = currentSelection.end; + final offset = end.offset; + final node = editorState.document.nodeAtPath(end.path)!; + final lengthOfNode = _endOffsetOfNode(node); + if (offset >= lengthOfNode) { + final nextNode = node.next; + if (nextNode != null) { + editorState.updateCursorSelection( + Selection.collapsed(Position(path: nextNode.path, offset: 0))); + } + return KeyEventResult.handled; + } + + editorState.updateCursorSelection( + Selection.collapsed(Position(path: end.path, offset: offset + 1))); + } else { + editorState.updateCursorSelection(currentSelection.collapse()); + } + 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 19604b0227..e3c262a360 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,4 +1,3 @@ -import 'package:flowy_editor/document/path.dart'; import 'package:flowy_editor/document/node.dart'; import 'package:flowy_editor/document/position.dart'; import 'package:flowy_editor/document/selection.dart'; @@ -49,7 +48,7 @@ mixin FlowySelectionService on State { /// [start] is the offset under the global coordinate system. Node? computeNodeInOffset(Node node, Offset offset); - /// Return the [Node]s in multiple selection. Emtpy list would be returned + /// Return the [Node]s in multiple selection. Empty list would be returned /// if no nodes are in range. /// /// [start] is the offset under the global coordinate system. @@ -136,8 +135,8 @@ class _FlowySelectionState extends State TapGestureRecognizer: GestureRecognizerFactoryWithHandlers( () => TapGestureRecognizer(), - (recongizer) { - recongizer.onTapDown = _onTapDown; + (recognizer) { + recognizer.onTapDown = _onTapDown; }, ) }, @@ -167,9 +166,9 @@ class _FlowySelectionState extends State if (end != null) { return computeNodesInRange(editorState.document.root, start, end); } else { - final reuslt = computeNodeInOffset(editorState.document.root, start); - if (reuslt != null) { - return [reuslt]; + final result = computeNodeInOffset(editorState.document.root, start); + if (result != null) { + return [result]; } } return []; @@ -253,8 +252,10 @@ class _FlowySelectionState extends State if (selectable != null) { final position = selectable.getPositionInOffset(tapOffset!); final selection = Selection.collapsed(position); - updateSelection(selection); + editorState.updateCursorSelection(selection); } + } else { + editorState.updateCursorSelection(null); } } @@ -283,7 +284,7 @@ class _FlowySelectionState extends State final selection = Selection( start: isDownward ? start : end, end: isDownward ? end : start); debugPrint('[_onPanUpdate] $selection'); - updateSelection(selection); + editorState.updateCursorSelection(selection); } } @@ -302,7 +303,7 @@ class _FlowySelectionState extends State _cursorOverlays ..forEach((overlay) => overlay.remove()) ..clear(); - // clear floating shortcusts + // clear floating shortcuts editorState.service.floatingShortcutServiceKey.currentState ?.unwrapOrNull() ?.hide(); From 53b982e7c9b50a26612e80d7ab44c444cc465277 Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Wed, 27 Jul 2022 15:58:25 +0800 Subject: [PATCH 2/6] feat: arrow up and down --- .../example/lib/plugin/image_node_widget.dart | 5 ++ .../lib/plugin/selected_text_node_widget.dart | 5 ++ .../lib/render/selection/cursor_widget.dart | 25 +++++++-- .../lib/render/selection/selectable.dart | 2 + .../arrow_keys_handler.dart | 22 +++++++- .../lib/service/selection_service.dart | 56 +++++++++++++++---- 6 files changed, 98 insertions(+), 17 deletions(-) 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 c5084df2fb..00a7fce8ad 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 @@ -63,6 +63,11 @@ class __ImageNodeWidgetState extends State<_ImageNodeWidget> with Selectable { throw UnimplementedError(); } + @override + Offset localToGlobal(Offset offset) { + throw UnimplementedError(); + } + @override Rect getCursorRectInPosition(Position position) { // TODO: implement getCursorRectInPosition 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 894f6b1848..3238decb81 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 @@ -70,6 +70,11 @@ class _SelectedTextNodeWidgetState extends State<_SelectedTextNodeWidget> ); } + @override + Offset localToGlobal(Offset offset) { + return _renderParagraph.localToGlobal(offset); + } + @override List getRectsInSelection(Selection selection) { assert(pathEquals(selection.start.path, selection.end.path)); diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/selection/cursor_widget.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/selection/cursor_widget.dart index 2ba42221f0..3e11073729 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/selection/cursor_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/selection/cursor_widget.dart @@ -17,10 +17,10 @@ class CursorWidget extends StatefulWidget { final LayerLink layerLink; @override - State createState() => _CursorWidgetState(); + State createState() => CursorWidgetState(); } -class _CursorWidgetState extends State { +class CursorWidgetState extends State { bool showCursor = true; late Timer timer; @@ -28,7 +28,17 @@ class _CursorWidgetState extends State { void initState() { super.initState(); - timer = Timer.periodic( + timer = _initTimer(); + } + + @override + void dispose() { + timer.cancel(); + super.dispose(); + } + + Timer _initTimer() { + return Timer.periodic( Duration(milliseconds: (widget.blinkingInterval * 1000).toInt()), (timer) { setState(() { @@ -37,10 +47,13 @@ class _CursorWidgetState extends State { }); } - @override - void dispose() { + /// force the cursor widget to show for a while + show() { + setState(() { + showCursor = true; + }); timer.cancel(); - super.dispose(); + timer = _initTimer(); } @override 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 4d155972df..df5649e320 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 @@ -23,6 +23,8 @@ mixin Selectable on State { Position getPositionInOffset(Offset start); Rect getCursorRectInPosition(Position position); + Offset localToGlobal(Offset offset); + Position start(); Position end(); diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/arrow_keys_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/arrow_keys_handler.dart index bdb473042d..30b295765e 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/arrow_keys_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/arrow_keys_handler.dart @@ -2,6 +2,7 @@ import 'package:flowy_editor/document/node.dart'; import 'package:flowy_editor/document/position.dart'; import 'package:flowy_editor/service/keyboard_service.dart'; import 'package:flowy_editor/document/selection.dart'; +import 'package:flowy_editor/extensions/node_extensions.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -26,7 +27,6 @@ FlowyKeyEventHandler arrowKeysHandler = (editorState, event) { } if (event.logicalKey == LogicalKeyboardKey.arrowLeft) { - // turn left if (currentSelection.isCollapsed) { final end = currentSelection.end; final offset = end.offset; @@ -67,6 +67,26 @@ FlowyKeyEventHandler arrowKeysHandler = (editorState, event) { editorState.updateCursorSelection(currentSelection.collapse()); } return KeyEventResult.handled; + } else if (event.logicalKey == LogicalKeyboardKey.arrowUp) { + final rects = editorState.service.selectionService.rects(); + if (rects.isEmpty) { + return KeyEventResult.handled; + } + final first = rects.first; + final firstOffset = Offset(first.left, first.top); + final hitOffset = firstOffset - Offset(0, first.height * 0.5); + editorState.service.selectionService.hit(hitOffset); + return KeyEventResult.handled; + } else if (event.logicalKey == LogicalKeyboardKey.arrowDown) { + final rects = editorState.service.selectionService.rects(); + if (rects.isEmpty) { + return KeyEventResult.handled; + } + final first = rects.last; + final firstOffset = Offset(first.right, first.bottom); + final hitOffset = firstOffset + Offset(0, first.height * 0.5); + editorState.service.selectionService.hit(hitOffset); + 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 e3c262a360..fee31ab4c0 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,6 +1,7 @@ import 'package:flowy_editor/document/node.dart'; import 'package:flowy_editor/document/position.dart'; import 'package:flowy_editor/document/selection.dart'; +import 'package:flowy_editor/render/selection/selectable.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'; @@ -26,6 +27,10 @@ mixin FlowySelectionService on State { /// void clearSelection(); + List rects(); + + hit(Offset? offset); + /// List getNodesInSelection(Selection selection); @@ -108,6 +113,8 @@ class _FlowySelectionState extends State /// Tap Offset? tapOffset; + final List _rects = []; + EditorState get editorState => widget.editorState; @override @@ -144,8 +151,13 @@ class _FlowySelectionState extends State ); } + List rects() { + return _rects; + } + @override void updateSelection(Selection selection) { + _rects.clear(); _clearSelection(); // cursor @@ -245,18 +257,29 @@ class _FlowySelectionState extends State tapOffset = details.globalPosition; - final nodes = getNodesInRange(tapOffset!); - if (nodes.isNotEmpty) { - assert(nodes.length == 1); - final selectable = nodes.first.selectable; - if (selectable != null) { - final position = selectable.getPositionInOffset(tapOffset!); - final selection = Selection.collapsed(position); - editorState.updateCursorSelection(selection); - } - } else { + hit(tapOffset); + } + + @override + hit(Offset? offset) { + if (offset == null) { editorState.updateCursorSelection(null); + return; } + final nodes = getNodesInRange(offset); + if (nodes.isEmpty) { + editorState.updateCursorSelection(null); + return; + } + assert(nodes.length == 1); + final selectable = nodes.first.selectable; + if (selectable == null) { + editorState.updateCursorSelection(null); + return; + } + final position = selectable.getPositionInOffset(offset); + final selection = Selection.collapsed(position); + editorState.updateCursorSelection(selection); } void _onPanStart(DragStartDetails details) { @@ -353,6 +376,7 @@ class _FlowySelectionState extends State final rects = selectable.getRectsInSelection(newSelection); for (final rect in rects) { + _rects.add(_transformRectToGlobal(selectable, rect)); final overlay = OverlayEntry( builder: ((context) => SelectionWidget( color: widget.selectionColor, @@ -367,6 +391,11 @@ class _FlowySelectionState extends State Overlay.of(context)?.insertAll(_selectionOverlays); } + Rect _transformRectToGlobal(Selectable selectable, Rect r) { + final Offset topLeft = selectable.localToGlobal(Offset(r.left, r.top)); + return Rect.fromLTWH(topLeft.dx, topLeft.dy, r.width, r.height); + } + void _updateCursor(Position position) { final node = editorState.document.root.childAtPath(position.path); @@ -380,6 +409,7 @@ class _FlowySelectionState extends State final selectable = node.selectable; final rect = selectable?.getCursorRectInPosition(position); if (rect != null) { + _rects.add(_transformRectToGlobal(selectable!, rect)); final cursor = OverlayEntry( builder: ((context) => CursorWidget( key: _cursorKey, @@ -390,9 +420,15 @@ class _FlowySelectionState extends State ); _cursorOverlays.add(cursor); Overlay.of(context)?.insertAll(_cursorOverlays); + _forceShowCursor(); } } + _forceShowCursor() { + final currentState = _cursorKey.currentState as CursorWidgetState?; + currentState?.show(); + } + List _selectedNodesInSelection(Node node, Selection selection) { List result = []; if (node.parent != null) { From 0ba7c53dad5dd1eafd7258b840a2dc33ad2fab3d Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Thu, 28 Jul 2022 14:41:29 +0800 Subject: [PATCH 3/6] feat: remove unused imports --- .../flowy_editor/lib/render/rich_text/flowy_rich_text.dart | 4 ++++ .../internal_key_event_handlers/arrow_keys_handler.dart | 1 - 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/flowy_rich_text.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/flowy_rich_text.dart index 66c87a2dd4..4731542ae2 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/flowy_rich_text.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/flowy_rich_text.dart @@ -255,6 +255,10 @@ class _FlowyRichTextState extends State with Selectable { return Rect.zero; } + Offset localToGlobal(Offset offset) { + return _renderParagraph.localToGlobal(offset); + } + TextSpan get _decorateTextSpanWithGlobalStyle => TextSpan( children: _textSpan.children ?.whereType() diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/arrow_keys_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/arrow_keys_handler.dart index 30b295765e..3bc3f5e0b5 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/arrow_keys_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/arrow_keys_handler.dart @@ -2,7 +2,6 @@ import 'package:flowy_editor/document/node.dart'; import 'package:flowy_editor/document/position.dart'; import 'package:flowy_editor/service/keyboard_service.dart'; import 'package:flowy_editor/document/selection.dart'; -import 'package:flowy_editor/extensions/node_extensions.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; From 883740d79a458a80a5431a4b9866bbf8fbc1b35d Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Thu, 28 Jul 2022 15:02:14 +0800 Subject: [PATCH 4/6] fix: assets of document --- frontend/app_flowy/packages/flowy_editor/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/app_flowy/packages/flowy_editor/pubspec.yaml b/frontend/app_flowy/packages/flowy_editor/pubspec.yaml index 08e51118d1..403ee2dddf 100644 --- a/frontend/app_flowy/packages/flowy_editor/pubspec.yaml +++ b/frontend/app_flowy/packages/flowy_editor/pubspec.yaml @@ -24,11 +24,11 @@ dev_dependencies: # The following section is specific to Flutter packages. flutter: - # To add assets to your package, add an assets section, like this: assets: - assets/images/uncheck.svg - assets/images/ + - assets/document.json # - images/a_dot_burr.jpeg # - images/a_dot_ham.jpeg # From 1d3e5a9e8b52a396a50c9ba96284416b728d0fb7 Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Thu, 28 Jul 2022 15:25:19 +0800 Subject: [PATCH 5/6] feat: handle shift keys --- .../arrow_keys_handler.dart | 97 ++++++++++++------- 1 file changed, 63 insertions(+), 34 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/arrow_keys_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/arrow_keys_handler.dart index 3bc3f5e0b5..cec123ad87 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/arrow_keys_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/arrow_keys_handler.dart @@ -1,7 +1,5 @@ -import 'package:flowy_editor/document/node.dart'; -import 'package:flowy_editor/document/position.dart'; +import 'package:flowy_editor/flowy_editor.dart'; import 'package:flowy_editor/service/keyboard_service.dart'; -import 'package:flowy_editor/document/selection.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -12,14 +10,65 @@ int _endOffsetOfNode(Node node) { return 0; } -FlowyKeyEventHandler arrowKeysHandler = (editorState, event) { - if (event.logicalKey != LogicalKeyboardKey.arrowUp && - event.logicalKey != LogicalKeyboardKey.arrowDown && - event.logicalKey != LogicalKeyboardKey.arrowLeft && - event.logicalKey != LogicalKeyboardKey.arrowRight) { +KeyEventResult _handleShiftKey(EditorState editorState, RawKeyEvent event) { + final currentSelection = editorState.cursorSelection; + if (currentSelection == null) { return KeyEventResult.ignored; } + if (event.logicalKey == LogicalKeyboardKey.arrowLeft) { + final leftPosition = _leftPosition(editorState, currentSelection.start); + if (leftPosition != null) { + editorState.updateCursorSelection( + Selection(start: leftPosition, end: currentSelection.end)); + } + return KeyEventResult.handled; + } else if (event.logicalKey == LogicalKeyboardKey.arrowRight) { + final rightPosition = _rightPosition(editorState, currentSelection.end); + if (rightPosition != null) { + editorState.updateCursorSelection( + Selection(start: currentSelection.start, end: rightPosition)); + } + return KeyEventResult.handled; + } + return KeyEventResult.ignored; +} + +Position? _leftPosition(EditorState editorState, Position position) { + final offset = position.offset; + if (offset == 0) { + final node = editorState.document.nodeAtPath(position.path)!; + final prevNode = node.previous; + if (prevNode != null) { + editorState.updateCursorSelection(Selection.collapsed( + Position(path: prevNode.path, offset: _endOffsetOfNode(prevNode)))); + } + return null; + } + + return Position(path: position.path, offset: offset - 1); +} + +Position? _rightPosition(EditorState editorState, Position position) { + final offset = position.offset; + final node = editorState.document.nodeAtPath(position.path)!; + final lengthOfNode = _endOffsetOfNode(node); + if (offset >= lengthOfNode) { + final nextNode = node.next; + if (nextNode != null) { + Position(path: nextNode.path, offset: 0); + } + return null; + } + + return Position(path: position.path, offset: offset + 1); +} + +FlowyKeyEventHandler arrowKeysHandler = (editorState, event) { + if (event.isShiftPressed) { + return _handleShiftKey(editorState, event); + } + final currentSelection = editorState.cursorSelection; if (currentSelection == null) { return KeyEventResult.ignored; @@ -27,19 +76,10 @@ FlowyKeyEventHandler arrowKeysHandler = (editorState, event) { if (event.logicalKey == LogicalKeyboardKey.arrowLeft) { if (currentSelection.isCollapsed) { - final end = currentSelection.end; - final offset = end.offset; - if (offset == 0) { - final node = editorState.document.nodeAtPath(end.path)!; - final prevNode = node.previous; - if (prevNode != null) { - editorState.updateCursorSelection(Selection.collapsed(Position( - path: prevNode.path, offset: _endOffsetOfNode(prevNode)))); - } - return KeyEventResult.handled; + final leftPosition = _leftPosition(editorState, currentSelection.start); + if (leftPosition != null) { + editorState.updateCursorSelection(Selection.collapsed(leftPosition)); } - editorState.updateCursorSelection( - Selection.collapsed(Position(path: end.path, offset: offset - 1))); } else { editorState .updateCursorSelection(currentSelection.collapse(atStart: true)); @@ -47,21 +87,10 @@ FlowyKeyEventHandler arrowKeysHandler = (editorState, event) { return KeyEventResult.handled; } else if (event.logicalKey == LogicalKeyboardKey.arrowRight) { if (currentSelection.isCollapsed) { - final end = currentSelection.end; - final offset = end.offset; - final node = editorState.document.nodeAtPath(end.path)!; - final lengthOfNode = _endOffsetOfNode(node); - if (offset >= lengthOfNode) { - final nextNode = node.next; - if (nextNode != null) { - editorState.updateCursorSelection( - Selection.collapsed(Position(path: nextNode.path, offset: 0))); - } - return KeyEventResult.handled; + final rightPosition = _rightPosition(editorState, currentSelection.end); + if (rightPosition != null) { + editorState.updateCursorSelection(Selection.collapsed(rightPosition)); } - - editorState.updateCursorSelection( - Selection.collapsed(Position(path: end.path, offset: offset + 1))); } else { editorState.updateCursorSelection(currentSelection.collapse()); } From b91c5d9c7b3e50b33d212723a53dbddd7ca6a51d Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Thu, 28 Jul 2022 15:33:42 +0800 Subject: [PATCH 6/6] refactor: add hitTest method for selection service --- .../arrow_keys_handler.dart | 138 ++++++++++-------- .../lib/service/selection_service.dart | 21 +-- 2 files changed, 92 insertions(+), 67 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/arrow_keys_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/arrow_keys_handler.dart index cec123ad87..7fbdf669b5 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/arrow_keys_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/arrow_keys_handler.dart @@ -10,6 +10,58 @@ int _endOffsetOfNode(Node node) { return 0; } +extension on Position { + Position? goLeft(EditorState editorState) { + if (offset == 0) { + final node = editorState.document.nodeAtPath(path)!; + final prevNode = node.previous; + if (prevNode != null) { + return Position( + path: prevNode.path, offset: _endOffsetOfNode(prevNode)); + } + return null; + } + + return Position(path: path, offset: offset - 1); + } + + Position? goRight(EditorState editorState) { + final node = editorState.document.nodeAtPath(path)!; + final lengthOfNode = _endOffsetOfNode(node); + if (offset >= lengthOfNode) { + final nextNode = node.next; + if (nextNode != null) { + return Position(path: nextNode.path, offset: 0); + } + return null; + } + + return Position(path: path, offset: offset + 1); + } +} + +Position? _goUp(EditorState editorState) { + final rects = editorState.service.selectionService.rects(); + if (rects.isEmpty) { + return null; + } + final first = rects.first; + final firstOffset = Offset(first.left, first.top); + final hitOffset = firstOffset - Offset(0, first.height * 0.5); + return editorState.service.selectionService.hitTest(hitOffset); +} + +Position? _goDown(EditorState editorState) { + final rects = editorState.service.selectionService.rects(); + if (rects.isEmpty) { + return null; + } + final first = rects.last; + final firstOffset = Offset(first.right, first.bottom); + final hitOffset = firstOffset + Offset(0, first.height * 0.5); + return editorState.service.selectionService.hitTest(hitOffset); +} + KeyEventResult _handleShiftKey(EditorState editorState, RawKeyEvent event) { final currentSelection = editorState.cursorSelection; if (currentSelection == null) { @@ -17,53 +69,33 @@ KeyEventResult _handleShiftKey(EditorState editorState, RawKeyEvent event) { } if (event.logicalKey == LogicalKeyboardKey.arrowLeft) { - final leftPosition = _leftPosition(editorState, currentSelection.start); - if (leftPosition != null) { - editorState.updateCursorSelection( - Selection(start: leftPosition, end: currentSelection.end)); - } + final leftPosition = currentSelection.end.goLeft(editorState); + editorState.updateCursorSelection(leftPosition == null + ? null + : Selection(start: currentSelection.start, end: leftPosition)); return KeyEventResult.handled; } else if (event.logicalKey == LogicalKeyboardKey.arrowRight) { - final rightPosition = _rightPosition(editorState, currentSelection.end); - if (rightPosition != null) { - editorState.updateCursorSelection( - Selection(start: currentSelection.start, end: rightPosition)); - } + final rightPosition = currentSelection.start.goRight(editorState); + editorState.updateCursorSelection(rightPosition == null + ? null + : Selection(start: rightPosition, end: currentSelection.end)); + return KeyEventResult.handled; + } else if (event.logicalKey == LogicalKeyboardKey.arrowUp) { + final position = _goUp(editorState); + editorState.updateCursorSelection(position == null + ? null + : Selection(start: position, end: currentSelection.end)); + return KeyEventResult.handled; + } else if (event.logicalKey == LogicalKeyboardKey.arrowDown) { + final position = _goDown(editorState); + editorState.updateCursorSelection(position == null + ? null + : Selection(start: currentSelection.start, end: position)); return KeyEventResult.handled; } return KeyEventResult.ignored; } -Position? _leftPosition(EditorState editorState, Position position) { - final offset = position.offset; - if (offset == 0) { - final node = editorState.document.nodeAtPath(position.path)!; - final prevNode = node.previous; - if (prevNode != null) { - editorState.updateCursorSelection(Selection.collapsed( - Position(path: prevNode.path, offset: _endOffsetOfNode(prevNode)))); - } - return null; - } - - return Position(path: position.path, offset: offset - 1); -} - -Position? _rightPosition(EditorState editorState, Position position) { - final offset = position.offset; - final node = editorState.document.nodeAtPath(position.path)!; - final lengthOfNode = _endOffsetOfNode(node); - if (offset >= lengthOfNode) { - final nextNode = node.next; - if (nextNode != null) { - Position(path: nextNode.path, offset: 0); - } - return null; - } - - return Position(path: position.path, offset: offset + 1); -} - FlowyKeyEventHandler arrowKeysHandler = (editorState, event) { if (event.isShiftPressed) { return _handleShiftKey(editorState, event); @@ -76,7 +108,7 @@ FlowyKeyEventHandler arrowKeysHandler = (editorState, event) { if (event.logicalKey == LogicalKeyboardKey.arrowLeft) { if (currentSelection.isCollapsed) { - final leftPosition = _leftPosition(editorState, currentSelection.start); + final leftPosition = currentSelection.start.goLeft(editorState); if (leftPosition != null) { editorState.updateCursorSelection(Selection.collapsed(leftPosition)); } @@ -87,7 +119,7 @@ FlowyKeyEventHandler arrowKeysHandler = (editorState, event) { return KeyEventResult.handled; } else if (event.logicalKey == LogicalKeyboardKey.arrowRight) { if (currentSelection.isCollapsed) { - final rightPosition = _rightPosition(editorState, currentSelection.end); + final rightPosition = currentSelection.end.goRight(editorState); if (rightPosition != null) { editorState.updateCursorSelection(Selection.collapsed(rightPosition)); } @@ -96,24 +128,14 @@ FlowyKeyEventHandler arrowKeysHandler = (editorState, event) { } return KeyEventResult.handled; } else if (event.logicalKey == LogicalKeyboardKey.arrowUp) { - final rects = editorState.service.selectionService.rects(); - if (rects.isEmpty) { - return KeyEventResult.handled; - } - final first = rects.first; - final firstOffset = Offset(first.left, first.top); - final hitOffset = firstOffset - Offset(0, first.height * 0.5); - editorState.service.selectionService.hit(hitOffset); + final position = _goUp(editorState); + editorState.updateCursorSelection( + position == null ? null : Selection.collapsed(position)); return KeyEventResult.handled; } else if (event.logicalKey == LogicalKeyboardKey.arrowDown) { - final rects = editorState.service.selectionService.rects(); - if (rects.isEmpty) { - return KeyEventResult.handled; - } - final first = rects.last; - final firstOffset = Offset(first.right, first.bottom); - final hitOffset = firstOffset + Offset(0, first.height * 0.5); - editorState.service.selectionService.hit(hitOffset); + final position = _goDown(editorState); + editorState.updateCursorSelection( + position == null ? null : Selection.collapsed(position)); return KeyEventResult.handled; } 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 c585c13bdd..3cfd1fd3f7 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 @@ -30,7 +30,7 @@ mixin FlowySelectionService on State { List rects(); - hit(Offset? offset); + Position? hitTest(Offset? offset); /// List getNodesInSelection(Selection selection); @@ -285,29 +285,32 @@ class _FlowySelectionState extends State tapOffset = details.globalPosition; - hit(tapOffset); + final position = hitTest(tapOffset); + if (position == null) { + return; + } + final selection = Selection.collapsed(position); + editorState.updateCursorSelection(selection); } @override - hit(Offset? offset) { + Position? hitTest(Offset? offset) { if (offset == null) { editorState.updateCursorSelection(null); - return; + return null; } final nodes = getNodesInRange(offset); if (nodes.isEmpty) { editorState.updateCursorSelection(null); - return; + return null; } assert(nodes.length == 1); final selectable = nodes.first.selectable; if (selectable == null) { editorState.updateCursorSelection(null); - return; + return null; } - final position = selectable.getPositionInOffset(offset); - final selection = Selection.collapsed(position); - editorState.updateCursorSelection(selection); + return selectable.getPositionInOffset(offset); } void _onPanStart(DragStartDetails details) {