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 fc440a8fa5..c5084df2fb 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 @@ -39,6 +39,18 @@ class __ImageNodeWidgetState extends State<_ImageNodeWidget> with Selectable { EditorState get editorState => widget.editorState; String get src => widget.node.attributes['image_src'] as String; + @override + Position end() { + // TODO: implement end + throw UnimplementedError(); + } + + @override + Position start() { + // TODO: implement start + throw UnimplementedError(); + } + @override List getRectsInSelection(Selection selection) { // TODO: implement getRectsInSelection @@ -63,16 +75,6 @@ class __ImageNodeWidgetState extends State<_ImageNodeWidget> with Selectable { throw UnimplementedError(); } - @override - Offset getBackwardOffset() { - return Offset.zero; - } - - @override - Offset getForwardOffset() { - 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 0f20f2fe3d..894f6b1848 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 @@ -107,28 +107,11 @@ class _SelectedTextNodeWidgetState extends State<_SelectedTextNodeWidget> } @override - Offset getBackwardOffset() { - final textSelection = _textSelection; - if (textSelection != null) { - final leftTextSelection = TextSelection.collapsed( - offset: max(0, textSelection.baseOffset - 1), - ); - return getOffsetByTextSelection(leftTextSelection); - } - return Offset.zero; - } + Position start() => Position(path: node.path, offset: 0); @override - Offset getForwardOffset() { - final textSelection = _textSelection; - if (textSelection != null) { - final leftTextSelection = TextSelection.collapsed( - offset: min(node.toRawString().length, textSelection.extentOffset + 1), - ); - return getOffsetByTextSelection(leftTextSelection); - } - return Offset.zero; - } + Position end() => + Position(path: node.path, offset: node.toRawString().length); @override Widget build(BuildContext context) { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/selection.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/selection.dart index 1734fabf24..a3919a21f6 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/document/selection.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/document/selection.dart @@ -31,6 +31,7 @@ class Selection { } bool get isCollapsed => start == end; + bool get isSingle => pathEquals(start.path, end.path); bool get isUpward => start.path >= end.path && !pathEquals(start.path, end.path); bool get isDownward => @@ -43,6 +44,8 @@ class Selection { ); } + Selection copy() => Selection(start: start, end: end); + @override String toString() => '[Selection] start = $start, end = $end'; } 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 f94d07e457..4d155972df 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,11 +23,8 @@ mixin Selectable on State { Position getPositionInOffset(Offset start); Rect getCursorRectInPosition(Position position); - /// Returns a backward offset of the current offset based on the cause. - Offset getBackwardOffset(/* Cause */); - - /// Returns a forward offset of the current offset based on the cause. - Offset getForwardOffset(/* Cause */); + Position start(); + Position end(); /// For [TextNode] only. /// 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 44fc9a146f..95496db2ea 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,5 +1,3 @@ -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'; @@ -12,26 +10,5 @@ FlowyKeyEventHandler arrowKeysHandler = (editorState, event) { 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?.getBackwardOffset(); - } else if (event.logicalKey == LogicalKeyboardKey.arrowRight) { - offset = selectable?.getForwardOffset(); - } - 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/selection_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart index e118f5ea62..19604b0227 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 @@ -14,15 +14,26 @@ import 'package:flutter/material.dart'; /// Process selection and cursor mixin FlowySelectionService on State { + /// Returns the currently selected [Node]s. /// + /// The order of the return is determined according to the selected order. List get currentSelectedNodes; + /// ------------------ Selection ------------------------ + /// void updateSelection(Selection selection); /// void clearSelection(); + /// + List getNodesInSelection(Selection selection); + + /// ------------------ Selection ------------------------ + + /// ------------------ Offset ------------------------ + /// Returns selected [Node]s. Empty list would be returned /// if no nodes are being selected. /// @@ -33,9 +44,6 @@ mixin FlowySelectionService on State { /// otherwise single selection. List getNodesInRange(Offset start, [Offset? end]); - /// - List getNodesInSelection(Selection selection); - /// Return the [Node] or [Null] in single selection. /// /// [start] is the offset under the global coordinate system. @@ -64,6 +72,8 @@ mixin FlowySelectionService on State { /// /// [start] is the offset under the global coordinate system. bool isNodeInOffset(Node node, Offset offset); + + /// ------------------ Offset ------------------------ } class FlowySelection extends StatefulWidget { @@ -101,9 +111,6 @@ class _FlowySelectionState extends State EditorState get editorState => widget.editorState; - Node? _selectedNodeInPostion(Node node, Position position) => - node.childAtPath(position.path); - @override List currentSelectedNodes = []; @@ -186,6 +193,17 @@ class _FlowySelectionState extends State @override List computeNodesInRange(Node node, Offset start, Offset end) { + final result = _computeNodesInRange(node, start, end); + if (start.dy <= end.dy) { + // downward + return result; + } else { + // upward + return result.reversed.toList(growable: false); + } + } + + List _computeNodesInRange(Node node, Offset start, Offset end) { List result = []; if (node.parent != null && node.key != null) { if (isNodeInRange(node, start, end)) { @@ -195,7 +213,6 @@ class _FlowySelectionState extends State for (final child in node.children) { result.addAll(computeNodesInRange(child, start, end)); } - // TODO: sort the result return result; } @@ -223,13 +240,12 @@ class _FlowySelectionState extends State } void _onTapDown(TapDownDetails details) { - debugPrint('on tap down'); - - // TODO: use setter to make them exclusive?? - tapOffset = details.globalPosition; + // clear old state. panStartOffset = null; panEndOffset = null; + tapOffset = details.globalPosition; + final nodes = getNodesInRange(tapOffset!); if (nodes.isNotEmpty) { assert(nodes.length == 1); @@ -243,38 +259,30 @@ class _FlowySelectionState extends State } void _onPanStart(DragStartDetails details) { - debugPrint('on pan start'); - - panStartOffset = details.globalPosition; + // clear old state. panEndOffset = null; tapOffset = null; + clearSelection(); + + panStartOffset = details.globalPosition; } void _onPanUpdate(DragUpdateDetails details) { - // debugPrint('on pan update'); - panEndOffset = details.globalPosition; - tapOffset = null; final nodes = getNodesInRange(panStartOffset!, panEndOffset!); final first = nodes.first.selectable; final last = nodes.last.selectable; + + // compute the selection in range. if (first != null && last != null) { - final Selection selection; - if (panStartOffset!.dy <= panEndOffset!.dy) { - // down - selection = Selection( - start: - first.getSelectionInRange(panStartOffset!, panEndOffset!).start, - end: last.getSelectionInRange(panStartOffset!, panEndOffset!).end, - ); - } else { - // up - selection = Selection( - start: last.getSelectionInRange(panStartOffset!, panEndOffset!).end, - end: first.getSelectionInRange(panStartOffset!, panEndOffset!).start, - ); - } + bool isDownward = panStartOffset!.dy <= panEndOffset!.dy; + final start = + first.getSelectionInRange(panStartOffset!, panEndOffset!).start; + final end = last.getSelectionInRange(panStartOffset!, panEndOffset!).end; + final selection = Selection( + start: isDownward ? start : end, end: isDownward ? end : start); + debugPrint('[_onPanUpdate] $selection'); updateSelection(selection); } } @@ -313,51 +321,32 @@ class _FlowySelectionState extends State continue; } - Selection newSelection; - // TODO: too complicate, need to refactor. - if (node is TextNode) { - if (pathEquals(selection.start.path, selection.end.path)) { - newSelection = selection.copyWith(); - } else { - if (index == 0) { - if (selection.isUpward) { - newSelection = selection.copyWith( - /// FIXME: make it better. - start: selection.end.copyWith(), - end: selection.end.copyWith(offset: node.toRawString().length), - ); - } else { - newSelection = selection.copyWith( - /// FIXME: make it better. - end: - selection.start.copyWith(offset: node.toRawString().length), - ); - } - } else if (index == nodes.length - 1) { - if (selection.isUpward) { - newSelection = selection.copyWith( - /// FIXME: make it better. - start: selection.start.copyWith(offset: 0), - end: selection.start.copyWith(), - ); - } else { - newSelection = selection.copyWith( - /// FIXME: make it better. - start: selection.end.copyWith(offset: 0), - ); - } + var newSelection = selection.copy(); + // In the case of multiple selections, + // we need to return a new selection for each selected node individually. + if (!selection.isSingle) { + // <> means selected. + // text: abcdopqr + if (index == 0) { + if (selection.isDownward) { + newSelection = selection.copyWith(end: selectable.end()); } else { - final position = Position(path: node.path); - newSelection = Selection( - start: position.copyWith(offset: 0), - end: position.copyWith(offset: node.toRawString().length), - ); + newSelection = selection.copyWith(start: selectable.start()); } + } else if (index == nodes.length - 1) { + if (selection.isDownward) { + newSelection = selection.copyWith(start: selectable.start()); + } else { + newSelection = selection.copyWith(end: selectable.end()); + } + } else { + newSelection = selection.copyWith( + start: selectable.start(), + end: selectable.end(), + ); } - } else { - newSelection = Selection.collapsed( - Position(path: node.path), - ); } final rects = selectable.getRectsInSelection(newSelection);