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) {