mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: #953 improve arrow keys handler
This commit is contained in:
parent
b61e3f4601
commit
f098c543e6
@ -46,9 +46,9 @@ class Selection {
|
||||
(start.path <= end.path && !pathEquals(start.path, end.path)) ||
|
||||
(isSingle && start.offset < end.offset);
|
||||
|
||||
Selection normalize() {
|
||||
Selection get normalize {
|
||||
if (isForward) {
|
||||
return Selection(start: end, end: start);
|
||||
return reversed;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
@ -32,7 +32,8 @@ mixin DefaultSelectable {
|
||||
Selection getSelectionInRange(Offset start, Offset end) =>
|
||||
forward.getSelectionInRange(start, end);
|
||||
|
||||
Offset localToGlobal(Offset offset) => forward.localToGlobal(offset);
|
||||
Offset localToGlobal(Offset offset) =>
|
||||
forward.localToGlobal(offset) - baseOffset;
|
||||
|
||||
Selection? getWorldBoundaryInOffset(Offset offset) =>
|
||||
forward.getWorldBoundaryInOffset(offset);
|
||||
|
@ -132,17 +132,24 @@ class _FlowyRichTextState extends State<FlowyRichText> with Selectable {
|
||||
|
||||
@override
|
||||
List<Rect> getRectsInSelection(Selection selection) {
|
||||
assert(pathEquals(selection.start.path, selection.end.path) &&
|
||||
assert(selection.isSingle &&
|
||||
pathEquals(selection.start.path, widget.textNode.path));
|
||||
|
||||
final textSelection = TextSelection(
|
||||
baseOffset: selection.start.offset,
|
||||
extentOffset: selection.end.offset,
|
||||
);
|
||||
return _renderParagraph
|
||||
final rects = _renderParagraph
|
||||
.getBoxesForSelection(textSelection, boxHeightStyle: BoxHeightStyle.max)
|
||||
.map((box) => box.toRect())
|
||||
.toList();
|
||||
.toList(growable: false);
|
||||
if (rects.isEmpty) {
|
||||
// If the rich text widget does not contain any text,
|
||||
// there will be no selection boxes,
|
||||
// so we need to return to the default selection.
|
||||
return [Rect.fromLTWH(0, 0, 0, _renderParagraph.size.height)];
|
||||
}
|
||||
return rects;
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -1,26 +1,225 @@
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_editor/src/extensions/node_extensions.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
int _endOffsetOfNode(Node node) {
|
||||
if (node is TextNode) {
|
||||
return node.delta.length;
|
||||
AppFlowyKeyEventHandler arrowKeysHandler = (editorState, event) {
|
||||
if (!_arrowKeys.contains(event.logicalKey)) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
return 0;
|
||||
|
||||
if (event.isMetaPressed && event.isShiftPressed) {
|
||||
return _arrowKeysWithMetaAndShift(editorState, event);
|
||||
} else if (event.isMetaPressed) {
|
||||
return _arrowKeysWithMeta(editorState, event);
|
||||
} else if (event.isShiftPressed) {
|
||||
return _arrowKeysWithShift(editorState, event);
|
||||
} else {
|
||||
return _arrowKeysOnly(editorState, event);
|
||||
}
|
||||
};
|
||||
|
||||
final _arrowKeys = [
|
||||
LogicalKeyboardKey.arrowLeft,
|
||||
LogicalKeyboardKey.arrowRight,
|
||||
LogicalKeyboardKey.arrowUp,
|
||||
LogicalKeyboardKey.arrowDown
|
||||
];
|
||||
|
||||
KeyEventResult _arrowKeysWithMetaAndShift(
|
||||
EditorState editorState, RawKeyEvent event) {
|
||||
if (!event.isMetaPressed ||
|
||||
!event.isShiftPressed ||
|
||||
!_arrowKeys.contains(event.logicalKey)) {
|
||||
assert(false);
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
|
||||
final nodes = editorState.service.selectionService.currentSelectedNodes;
|
||||
final selection = editorState.service.selectionService.currentSelection.value;
|
||||
if (nodes.isEmpty || selection == null) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
|
||||
var start = selection.start;
|
||||
var end = selection.end;
|
||||
if (event.logicalKey == LogicalKeyboardKey.arrowLeft) {
|
||||
final position = nodes.first.selectable?.start();
|
||||
if (position != null) {
|
||||
end = position;
|
||||
}
|
||||
} else if (event.logicalKey == LogicalKeyboardKey.arrowRight) {
|
||||
final position = nodes.first.selectable?.end();
|
||||
if (position != null) {
|
||||
end = position;
|
||||
}
|
||||
} else if (event.logicalKey == LogicalKeyboardKey.arrowUp) {
|
||||
final position = editorState.document.root.children
|
||||
.whereType<TextNode>()
|
||||
.first
|
||||
.selectable
|
||||
?.start();
|
||||
if (position != null) {
|
||||
end = position;
|
||||
}
|
||||
} else if (event.logicalKey == LogicalKeyboardKey.arrowDown) {
|
||||
final position = editorState.document.root.children
|
||||
.whereType<TextNode>()
|
||||
.last
|
||||
.selectable
|
||||
?.end();
|
||||
if (position != null) {
|
||||
end = position;
|
||||
}
|
||||
}
|
||||
editorState.service.selectionService.updateSelection(
|
||||
selection.copyWith(start: start, end: end),
|
||||
);
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
|
||||
// Move the cursor to top, bottom, left and right of the document.
|
||||
KeyEventResult _arrowKeysWithMeta(EditorState editorState, RawKeyEvent event) {
|
||||
if (!event.isMetaPressed ||
|
||||
event.isShiftPressed ||
|
||||
!_arrowKeys.contains(event.logicalKey)) {
|
||||
assert(false);
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
|
||||
final nodes = editorState.service.selectionService.currentSelectedNodes;
|
||||
if (nodes.isEmpty) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
Position? position;
|
||||
if (event.logicalKey == LogicalKeyboardKey.arrowLeft) {
|
||||
position = nodes.first.selectable?.start();
|
||||
} else if (event.logicalKey == LogicalKeyboardKey.arrowRight) {
|
||||
position = nodes.last.selectable?.end();
|
||||
} else if (event.logicalKey == LogicalKeyboardKey.arrowUp) {
|
||||
position = editorState.document.root.children
|
||||
.whereType<TextNode>()
|
||||
.first
|
||||
.selectable
|
||||
?.start();
|
||||
} else if (event.logicalKey == LogicalKeyboardKey.arrowDown) {
|
||||
position = editorState.document.root.children
|
||||
.whereType<TextNode>()
|
||||
.last
|
||||
.selectable
|
||||
?.end();
|
||||
}
|
||||
if (position == null) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
editorState.service.selectionService.updateSelection(
|
||||
Selection.collapsed(position),
|
||||
);
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
|
||||
KeyEventResult _arrowKeysWithShift(EditorState editorState, RawKeyEvent event) {
|
||||
if (event.isMetaPressed ||
|
||||
!event.isShiftPressed ||
|
||||
!_arrowKeys.contains(event.logicalKey)) {
|
||||
assert(false);
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
|
||||
final nodes = editorState.service.selectionService.currentSelectedNodes;
|
||||
final selection = editorState.service.selectionService.currentSelection.value;
|
||||
if (nodes.isEmpty || selection == null) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
Position? end;
|
||||
if (event.logicalKey == LogicalKeyboardKey.arrowLeft) {
|
||||
end = selection.end.goLeft(editorState);
|
||||
} else if (event.logicalKey == LogicalKeyboardKey.arrowRight) {
|
||||
end = selection.end.goRight(editorState);
|
||||
} else if (event.logicalKey == LogicalKeyboardKey.arrowUp) {
|
||||
end = _goUp(editorState);
|
||||
} else if (event.logicalKey == LogicalKeyboardKey.arrowDown) {
|
||||
end = _goDown(editorState);
|
||||
}
|
||||
if (end == null) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
editorState.service.selectionService
|
||||
.updateSelection(selection.copyWith(end: end));
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
|
||||
KeyEventResult _arrowKeysOnly(EditorState editorState, RawKeyEvent event) {
|
||||
if (event.isMetaPressed ||
|
||||
event.isShiftPressed ||
|
||||
!_arrowKeys.contains(event.logicalKey)) {
|
||||
assert(false);
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
|
||||
final nodes = editorState.service.selectionService.currentSelectedNodes;
|
||||
final selection =
|
||||
editorState.service.selectionService.currentSelection.value?.normalize;
|
||||
if (nodes.isEmpty || selection == null) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
if (event.logicalKey == LogicalKeyboardKey.arrowLeft) {
|
||||
if (selection.isCollapsed) {
|
||||
final leftPosition = selection.start.goLeft(editorState);
|
||||
if (leftPosition != null) {
|
||||
editorState.service.selectionService.updateSelection(
|
||||
Selection.collapsed(leftPosition),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
editorState.service.selectionService.updateSelection(
|
||||
Selection.collapsed(selection.start),
|
||||
);
|
||||
}
|
||||
return KeyEventResult.handled;
|
||||
} else if (event.logicalKey == LogicalKeyboardKey.arrowRight) {
|
||||
if (selection.isCollapsed) {
|
||||
final rightPosition = selection.start.goRight(editorState);
|
||||
if (rightPosition != null) {
|
||||
editorState.service.selectionService.updateSelection(
|
||||
Selection.collapsed(rightPosition),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
editorState.service.selectionService.updateSelection(
|
||||
Selection.collapsed(selection.end),
|
||||
);
|
||||
}
|
||||
return KeyEventResult.handled;
|
||||
} else if (event.logicalKey == LogicalKeyboardKey.arrowUp) {
|
||||
final upPosition = _goUp(editorState);
|
||||
editorState.updateCursorSelection(
|
||||
upPosition == null ? null : Selection.collapsed(upPosition),
|
||||
);
|
||||
return KeyEventResult.handled;
|
||||
} else if (event.logicalKey == LogicalKeyboardKey.arrowDown) {
|
||||
final downPosition = _goDown(editorState);
|
||||
editorState.updateCursorSelection(
|
||||
downPosition == null ? null : Selection.collapsed(downPosition),
|
||||
);
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
|
||||
extension on Position {
|
||||
Position? goLeft(EditorState editorState) {
|
||||
final node = editorState.document.nodeAtPath(path)!;
|
||||
final node = editorState.document.nodeAtPath(path);
|
||||
if (node == null) {
|
||||
return null;
|
||||
}
|
||||
if (offset == 0) {
|
||||
final prevNode = node.previous;
|
||||
if (prevNode != null) {
|
||||
return Position(
|
||||
path: prevNode.path, offset: _endOffsetOfNode(prevNode));
|
||||
final previousEnd = node.previous?.selectable?.end();
|
||||
if (previousEnd != null) {
|
||||
return previousEnd;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
if (node is TextNode) {
|
||||
return Position(path: path, offset: node.delta.prevRunePosition(offset));
|
||||
} else {
|
||||
@ -29,16 +228,18 @@ extension on Position {
|
||||
}
|
||||
|
||||
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);
|
||||
final node = editorState.document.nodeAtPath(path);
|
||||
if (node == null) {
|
||||
return null;
|
||||
}
|
||||
final end = node.selectable?.end();
|
||||
if (end != null && offset >= end.offset) {
|
||||
final nextStart = node.next?.selectable?.start();
|
||||
if (nextStart != null) {
|
||||
return nextStart;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
if (node is TextNode) {
|
||||
return Position(path: path, offset: node.delta.nextRunePosition(offset));
|
||||
} else {
|
||||
@ -48,106 +249,43 @@ extension on Position {
|
||||
}
|
||||
|
||||
Position? _goUp(EditorState editorState) {
|
||||
final selection = editorState.service.selectionService.currentSelection.value;
|
||||
final rects = editorState.service.selectionService.selectionRects;
|
||||
if (rects.isEmpty) {
|
||||
if (rects.isEmpty || selection == null) {
|
||||
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.getPositionInOffset(hitOffset);
|
||||
Offset offset;
|
||||
if (selection.isBackward) {
|
||||
final rect = rects.reduce(
|
||||
(current, next) => current.bottom >= next.bottom ? current : next,
|
||||
);
|
||||
offset = rect.topRight.translate(0, -rect.height);
|
||||
} else {
|
||||
final rect = rects.reduce(
|
||||
(current, next) => current.top <= next.top ? current : next,
|
||||
);
|
||||
offset = rect.topLeft.translate(0, -rect.height);
|
||||
}
|
||||
return editorState.service.selectionService.getPositionInOffset(offset);
|
||||
}
|
||||
|
||||
Position? _goDown(EditorState editorState) {
|
||||
final selection = editorState.service.selectionService.currentSelection.value;
|
||||
final rects = editorState.service.selectionService.selectionRects;
|
||||
if (rects.isEmpty) {
|
||||
if (rects.isEmpty || selection == null) {
|
||||
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.getPositionInOffset(hitOffset);
|
||||
Offset offset;
|
||||
if (selection.isBackward) {
|
||||
final rect = rects.reduce(
|
||||
(current, next) => current.bottom >= next.bottom ? current : next,
|
||||
);
|
||||
offset = rect.bottomRight.translate(0, rect.height);
|
||||
} else {
|
||||
final rect = rects.reduce(
|
||||
(current, next) => current.top <= next.top ? current : next,
|
||||
);
|
||||
offset = rect.bottomLeft.translate(0, rect.height);
|
||||
}
|
||||
return editorState.service.selectionService.getPositionInOffset(offset);
|
||||
}
|
||||
|
||||
KeyEventResult _handleShiftKey(EditorState editorState, RawKeyEvent event) {
|
||||
final currentSelection = editorState.cursorSelection;
|
||||
if (currentSelection == null) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
|
||||
if (event.logicalKey == LogicalKeyboardKey.arrowLeft) {
|
||||
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 = 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;
|
||||
}
|
||||
|
||||
AppFlowyKeyEventHandler arrowKeysHandler = (editorState, event) {
|
||||
if (event.isShiftPressed) {
|
||||
return _handleShiftKey(editorState, event);
|
||||
}
|
||||
|
||||
final currentSelection = editorState.cursorSelection;
|
||||
if (currentSelection == null) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
|
||||
if (event.logicalKey == LogicalKeyboardKey.arrowLeft) {
|
||||
if (currentSelection.isCollapsed) {
|
||||
final leftPosition = currentSelection.start.goLeft(editorState);
|
||||
if (leftPosition != null) {
|
||||
editorState.updateCursorSelection(Selection.collapsed(leftPosition));
|
||||
}
|
||||
} else {
|
||||
editorState.updateCursorSelection(
|
||||
currentSelection.collapse(atStart: currentSelection.isBackward),
|
||||
);
|
||||
}
|
||||
return KeyEventResult.handled;
|
||||
} else if (event.logicalKey == LogicalKeyboardKey.arrowRight) {
|
||||
if (currentSelection.isCollapsed) {
|
||||
final rightPosition = currentSelection.end.goRight(editorState);
|
||||
if (rightPosition != null) {
|
||||
editorState.updateCursorSelection(Selection.collapsed(rightPosition));
|
||||
}
|
||||
} else {
|
||||
editorState.updateCursorSelection(
|
||||
currentSelection.collapse(atStart: !currentSelection.isBackward),
|
||||
);
|
||||
}
|
||||
return KeyEventResult.handled;
|
||||
} else if (event.logicalKey == LogicalKeyboardKey.arrowUp) {
|
||||
final position = _goUp(editorState);
|
||||
editorState.updateCursorSelection(
|
||||
position == null ? null : Selection.collapsed(position));
|
||||
return KeyEventResult.handled;
|
||||
} else if (event.logicalKey == LogicalKeyboardKey.arrowDown) {
|
||||
final position = _goDown(editorState);
|
||||
editorState.updateCursorSelection(
|
||||
position == null ? null : Selection.collapsed(position));
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
|
||||
return KeyEventResult.ignored;
|
||||
};
|
||||
|
@ -1,13 +1,12 @@
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_editor/src/infra/html_converter.dart';
|
||||
import 'package:appflowy_editor/src/document/node_iterator.dart';
|
||||
import 'package:appflowy_editor/src/infra/log.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:rich_clipboard/rich_clipboard.dart';
|
||||
|
||||
_handleCopy(EditorState editorState) async {
|
||||
final selection = editorState.cursorSelection?.normalize();
|
||||
final selection = editorState.cursorSelection?.normalize;
|
||||
if (selection == null || selection.isCollapsed) {
|
||||
return;
|
||||
}
|
||||
@ -43,7 +42,7 @@ _handleCopy(EditorState editorState) async {
|
||||
}
|
||||
|
||||
_pasteHTML(EditorState editorState, String html) {
|
||||
final selection = editorState.cursorSelection?.normalize();
|
||||
final selection = editorState.cursorSelection?.normalize;
|
||||
if (selection == null) {
|
||||
return;
|
||||
}
|
||||
@ -191,7 +190,7 @@ Delta _lineContentToDelta(String lineContent) {
|
||||
}
|
||||
|
||||
_handlePastePlainText(EditorState editorState, String plainText) {
|
||||
final selection = editorState.cursorSelection?.normalize();
|
||||
final selection = editorState.cursorSelection?.normalize;
|
||||
if (selection == null) {
|
||||
return;
|
||||
}
|
||||
@ -256,7 +255,7 @@ _handleCut(EditorState editorState) {
|
||||
}
|
||||
|
||||
_deleteSelectedContent(EditorState editorState) {
|
||||
final selection = editorState.cursorSelection?.normalize();
|
||||
final selection = editorState.cursorSelection?.normalize;
|
||||
if (selection == null || selection.isCollapsed) {
|
||||
return;
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import 'package:appflowy_editor/src/service/internal_key_event_handlers/arrow_keys_handler.dart';
|
||||
import 'package:appflowy_editor/src/service/internal_key_event_handlers/copy_paste_handler.dart';
|
||||
import 'package:appflowy_editor/src/service/internal_key_event_handlers/backspace_handler.dart';
|
||||
import 'package:appflowy_editor/src/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart';
|
||||
import 'package:appflowy_editor/src/service/internal_key_event_handlers/arrow_keys_handler.dart';
|
||||
import 'package:appflowy_editor/src/service/internal_key_event_handlers/redo_undo_handler.dart';
|
||||
import 'package:appflowy_editor/src/service/internal_key_event_handlers/slash_handler.dart';
|
||||
import 'package:appflowy_editor/src/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart';
|
||||
@ -13,6 +13,7 @@ import 'package:appflowy_editor/src/service/keyboard_service.dart';
|
||||
List<AppFlowyKeyEventHandler> defaultKeyEventHandlers = [
|
||||
deleteTextHandler,
|
||||
slashShortcutHandler,
|
||||
// arrowKeysHandler,
|
||||
arrowKeysHandler,
|
||||
copyPasteKeysHandler,
|
||||
redoUndoKeysHandler,
|
||||
|
@ -0,0 +1,153 @@
|
||||
import 'package:appflowy_editor/appflowy_editor.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;
|
||||
}
|
||||
|
||||
extension on Position {
|
||||
Position? goLeft(EditorState editorState) {
|
||||
final node = editorState.document.nodeAtPath(path)!;
|
||||
if (offset == 0) {
|
||||
final prevNode = node.previous;
|
||||
if (prevNode != null) {
|
||||
return Position(
|
||||
path: prevNode.path, offset: _endOffsetOfNode(prevNode));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
if (node is TextNode) {
|
||||
return Position(path: path, offset: node.delta.prevRunePosition(offset));
|
||||
} else {
|
||||
return Position(path: path, offset: offset);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
if (node is TextNode) {
|
||||
return Position(path: path, offset: node.delta.nextRunePosition(offset));
|
||||
} else {
|
||||
return Position(path: path, offset: offset);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Position? _goUp(EditorState editorState) {
|
||||
final rects = editorState.service.selectionService.selectionRects;
|
||||
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.getPositionInOffset(hitOffset);
|
||||
}
|
||||
|
||||
Position? _goDown(EditorState editorState) {
|
||||
final rects = editorState.service.selectionService.selectionRects;
|
||||
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.getPositionInOffset(hitOffset);
|
||||
}
|
||||
|
||||
KeyEventResult _handleShiftKey(EditorState editorState, RawKeyEvent event) {
|
||||
final currentSelection = editorState.cursorSelection;
|
||||
if (currentSelection == null) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
|
||||
if (event.logicalKey == LogicalKeyboardKey.arrowLeft) {
|
||||
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 = 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;
|
||||
}
|
||||
|
||||
AppFlowyKeyEventHandler arrowKeysHandler = (editorState, event) {
|
||||
if (event.isShiftPressed) {
|
||||
return _handleShiftKey(editorState, event);
|
||||
}
|
||||
|
||||
final currentSelection = editorState.cursorSelection;
|
||||
if (currentSelection == null) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
|
||||
if (event.logicalKey == LogicalKeyboardKey.arrowLeft) {
|
||||
if (currentSelection.isCollapsed) {
|
||||
final leftPosition = currentSelection.start.goLeft(editorState);
|
||||
if (leftPosition != null) {
|
||||
editorState.updateCursorSelection(Selection.collapsed(leftPosition));
|
||||
}
|
||||
} else {
|
||||
editorState.updateCursorSelection(
|
||||
currentSelection.collapse(atStart: currentSelection.isBackward),
|
||||
);
|
||||
}
|
||||
return KeyEventResult.handled;
|
||||
} else if (event.logicalKey == LogicalKeyboardKey.arrowRight) {
|
||||
if (currentSelection.isCollapsed) {
|
||||
final rightPosition = currentSelection.end.goRight(editorState);
|
||||
if (rightPosition != null) {
|
||||
editorState.updateCursorSelection(Selection.collapsed(rightPosition));
|
||||
}
|
||||
} else {
|
||||
editorState.updateCursorSelection(
|
||||
currentSelection.collapse(atStart: !currentSelection.isBackward),
|
||||
);
|
||||
}
|
||||
return KeyEventResult.handled;
|
||||
} else if (event.logicalKey == LogicalKeyboardKey.arrowUp) {
|
||||
final position = _goUp(editorState);
|
||||
editorState.updateCursorSelection(
|
||||
position == null ? null : Selection.collapsed(position));
|
||||
return KeyEventResult.handled;
|
||||
} else if (event.logicalKey == LogicalKeyboardKey.arrowDown) {
|
||||
final position = _goDown(editorState);
|
||||
editorState.updateCursorSelection(
|
||||
position == null ? null : Selection.collapsed(position));
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
|
||||
return KeyEventResult.ignored;
|
||||
};
|
@ -25,10 +25,6 @@ AppFlowyKeyEventHandler slashShortcutHandler = (editorState, event) {
|
||||
if (selection == null || context == null || selectable == null) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
final selectionRects = editorState.service.selectionService.selectionRects;
|
||||
if (selectionRects.isEmpty) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
TransactionBuilder(editorState)
|
||||
..replaceText(textNode, selection.start.offset,
|
||||
selection.end.offset - selection.start.offset, event.character ?? '')
|
||||
|
@ -348,10 +348,8 @@ class _AppFlowySelectionState extends State<AppFlowySelection>
|
||||
|
||||
final backwardNodes =
|
||||
selection.isBackward ? nodes : nodes.reversed.toList(growable: false);
|
||||
final backwardSelection = selection.isBackward
|
||||
? selection
|
||||
: selection.copyWith(start: selection.end, end: selection.start);
|
||||
assert(backwardSelection.isBackward);
|
||||
final normalizedSelection = selection.normalize;
|
||||
assert(normalizedSelection.isBackward);
|
||||
|
||||
for (var i = 0; i < backwardNodes.length; i++) {
|
||||
final node = backwardNodes[i];
|
||||
@ -360,7 +358,7 @@ class _AppFlowySelectionState extends State<AppFlowySelection>
|
||||
continue;
|
||||
}
|
||||
|
||||
var newSelection = backwardSelection.copy();
|
||||
var newSelection = normalizedSelection.copy();
|
||||
|
||||
/// In the case of multiple selections,
|
||||
/// we need to return a new selection for each selected node individually.
|
||||
@ -370,7 +368,7 @@ class _AppFlowySelectionState extends State<AppFlowySelection>
|
||||
/// text: ghijkl
|
||||
/// text: mn>opqr
|
||||
///
|
||||
if (!backwardSelection.isSingle) {
|
||||
if (!normalizedSelection.isSingle) {
|
||||
if (i == 0) {
|
||||
newSelection = newSelection.copyWith(end: selectable.end());
|
||||
} else if (i == nodes.length - 1) {
|
||||
|
@ -103,6 +103,9 @@ extension on LogicalKeyboardKey {
|
||||
if (this == LogicalKeyboardKey.slash) {
|
||||
return PhysicalKeyboardKey.slash;
|
||||
}
|
||||
if (this == LogicalKeyboardKey.arrowUp) {
|
||||
return PhysicalKeyboardKey.arrowUp;
|
||||
}
|
||||
if (this == LogicalKeyboardKey.arrowDown) {
|
||||
return PhysicalKeyboardKey.arrowDown;
|
||||
}
|
||||
|
@ -58,6 +58,219 @@ void main() async {
|
||||
(tester) async {
|
||||
await _testPressArrowKeyInNotCollapsedSelection(tester, false);
|
||||
});
|
||||
|
||||
testWidgets('Presses arrow left/right + shift in collapsed selection',
|
||||
(tester) async {
|
||||
const text = 'Welcome to Appflowy';
|
||||
final editor = tester.editor
|
||||
..insertTextNode(text)
|
||||
..insertTextNode(text);
|
||||
await editor.startTesting();
|
||||
const offset = 8;
|
||||
final selection = Selection.single(path: [1], startOffset: offset);
|
||||
await editor.updateSelection(selection);
|
||||
for (var i = offset - 1; i >= 0; i--) {
|
||||
await editor.pressLogicKey(
|
||||
LogicalKeyboardKey.arrowLeft,
|
||||
isShiftPressed: true,
|
||||
);
|
||||
expect(
|
||||
editor.documentSelection,
|
||||
selection.copyWith(
|
||||
end: Position(path: [1], offset: i),
|
||||
),
|
||||
);
|
||||
}
|
||||
for (var i = text.length; i >= 0; i--) {
|
||||
await editor.pressLogicKey(
|
||||
LogicalKeyboardKey.arrowLeft,
|
||||
isShiftPressed: true,
|
||||
);
|
||||
expect(
|
||||
editor.documentSelection,
|
||||
selection.copyWith(
|
||||
end: Position(path: [0], offset: i),
|
||||
),
|
||||
);
|
||||
}
|
||||
for (var i = 1; i <= text.length; i++) {
|
||||
await editor.pressLogicKey(
|
||||
LogicalKeyboardKey.arrowRight,
|
||||
isShiftPressed: true,
|
||||
);
|
||||
expect(
|
||||
editor.documentSelection,
|
||||
selection.copyWith(
|
||||
end: Position(path: [0], offset: i),
|
||||
),
|
||||
);
|
||||
}
|
||||
for (var i = 0; i < text.length; i++) {
|
||||
await editor.pressLogicKey(
|
||||
LogicalKeyboardKey.arrowRight,
|
||||
isShiftPressed: true,
|
||||
);
|
||||
expect(
|
||||
editor.documentSelection,
|
||||
selection.copyWith(
|
||||
end: Position(path: [1], offset: i),
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'Presses arrow left/right + shift in not collapsed and backward selection',
|
||||
(tester) async {
|
||||
const text = 'Welcome to Appflowy';
|
||||
final editor = tester.editor
|
||||
..insertTextNode(text)
|
||||
..insertTextNode(text);
|
||||
await editor.startTesting();
|
||||
const start = 8;
|
||||
const end = 12;
|
||||
final selection = Selection.single(
|
||||
path: [0],
|
||||
startOffset: start,
|
||||
endOffset: end,
|
||||
);
|
||||
await editor.updateSelection(selection);
|
||||
for (var i = end + 1; i <= text.length; i++) {
|
||||
await editor.pressLogicKey(
|
||||
LogicalKeyboardKey.arrowRight,
|
||||
isShiftPressed: true,
|
||||
);
|
||||
expect(
|
||||
editor.documentSelection,
|
||||
selection.copyWith(
|
||||
end: Position(path: [0], offset: i),
|
||||
),
|
||||
);
|
||||
}
|
||||
for (var i = text.length - 1; i >= 0; i--) {
|
||||
await editor.pressLogicKey(
|
||||
LogicalKeyboardKey.arrowLeft,
|
||||
isShiftPressed: true,
|
||||
);
|
||||
expect(
|
||||
editor.documentSelection,
|
||||
selection.copyWith(
|
||||
end: Position(path: [0], offset: i),
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'Presses arrow left/right + command in not collapsed and forward selection',
|
||||
(tester) async {
|
||||
const text = 'Welcome to Appflowy';
|
||||
final editor = tester.editor
|
||||
..insertTextNode(text)
|
||||
..insertTextNode(text);
|
||||
await editor.startTesting();
|
||||
const start = 12;
|
||||
const end = 8;
|
||||
final selection = Selection.single(
|
||||
path: [0],
|
||||
startOffset: start,
|
||||
endOffset: end,
|
||||
);
|
||||
await editor.updateSelection(selection);
|
||||
for (var i = end - 1; i >= 0; i--) {
|
||||
await editor.pressLogicKey(
|
||||
LogicalKeyboardKey.arrowLeft,
|
||||
isShiftPressed: true,
|
||||
);
|
||||
expect(
|
||||
editor.documentSelection,
|
||||
selection.copyWith(
|
||||
end: Position(path: [0], offset: i),
|
||||
),
|
||||
);
|
||||
}
|
||||
for (var i = 1; i <= text.length; i++) {
|
||||
await editor.pressLogicKey(
|
||||
LogicalKeyboardKey.arrowRight,
|
||||
isShiftPressed: true,
|
||||
);
|
||||
expect(
|
||||
editor.documentSelection,
|
||||
selection.copyWith(
|
||||
end: Position(path: [0], offset: i),
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
testWidgets('Presses arrow left/right/up/down + meta in collapsed selection',
|
||||
(tester) async {
|
||||
await _testPressArrowKeyWithMetaInSelection(tester, true, false);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'Presses arrow left/right/up/down + meta in not collapsed and backward selection',
|
||||
(tester) async {
|
||||
await _testPressArrowKeyWithMetaInSelection(tester, false, true);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'Presses arrow left/right/up/down + meta in not collapsed and forward selection',
|
||||
(tester) async {
|
||||
await _testPressArrowKeyWithMetaInSelection(tester, false, false);
|
||||
});
|
||||
|
||||
testWidgets('Presses arrow up/down + shift in not collapsed selection',
|
||||
(tester) async {
|
||||
const text = 'Welcome to Appflowy 😁';
|
||||
final editor = tester.editor
|
||||
..insertTextNode(text)
|
||||
..insertTextNode(text)
|
||||
..insertTextNode(null)
|
||||
..insertTextNode(text)
|
||||
..insertTextNode(null)
|
||||
..insertTextNode(text)
|
||||
..insertTextNode(text);
|
||||
await editor.startTesting();
|
||||
final selection = Selection.single(path: [3], startOffset: 8);
|
||||
await editor.updateSelection(selection);
|
||||
for (int i = 0; i < 3; i++) {
|
||||
await editor.pressLogicKey(
|
||||
LogicalKeyboardKey.arrowUp,
|
||||
isShiftPressed: true,
|
||||
);
|
||||
}
|
||||
expect(
|
||||
editor.documentSelection,
|
||||
selection.copyWith(
|
||||
end: Position(path: [0], offset: 0),
|
||||
),
|
||||
);
|
||||
for (int i = 0; i < 7; i++) {
|
||||
await editor.pressLogicKey(
|
||||
LogicalKeyboardKey.arrowDown,
|
||||
isShiftPressed: true,
|
||||
);
|
||||
}
|
||||
expect(
|
||||
editor.documentSelection,
|
||||
selection.copyWith(
|
||||
end: Position(path: [6], offset: 0),
|
||||
),
|
||||
);
|
||||
for (int i = 0; i < 3; i++) {
|
||||
await editor.pressLogicKey(
|
||||
LogicalKeyboardKey.arrowUp,
|
||||
isShiftPressed: true,
|
||||
);
|
||||
}
|
||||
expect(
|
||||
editor.documentSelection,
|
||||
selection.copyWith(
|
||||
end: Position(path: [3], offset: 0),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _testPressArrowKeyInNotCollapsedSelection(
|
||||
@ -82,3 +295,72 @@ Future<void> _testPressArrowKeyInNotCollapsedSelection(
|
||||
await editor.pressLogicKey(LogicalKeyboardKey.arrowRight);
|
||||
expect(editor.documentSelection?.end, end);
|
||||
}
|
||||
|
||||
Future<void> _testPressArrowKeyWithMetaInSelection(
|
||||
WidgetTester tester,
|
||||
bool isSingle,
|
||||
bool isBackward,
|
||||
) async {
|
||||
const text = 'Welcome to Appflowy';
|
||||
final editor = tester.editor
|
||||
..insertTextNode(text)
|
||||
..insertTextNode(text);
|
||||
await editor.startTesting();
|
||||
Selection selection;
|
||||
if (isSingle) {
|
||||
selection = Selection.single(
|
||||
path: [0],
|
||||
startOffset: 8,
|
||||
);
|
||||
} else {
|
||||
if (isBackward) {
|
||||
selection = Selection.single(
|
||||
path: [0],
|
||||
startOffset: 8,
|
||||
endOffset: text.length,
|
||||
);
|
||||
} else {
|
||||
selection = Selection.single(
|
||||
path: [0],
|
||||
startOffset: text.length,
|
||||
endOffset: 8,
|
||||
);
|
||||
}
|
||||
}
|
||||
await editor.updateSelection(selection);
|
||||
await editor.pressLogicKey(
|
||||
LogicalKeyboardKey.arrowLeft,
|
||||
isMetaPressed: true,
|
||||
);
|
||||
expect(
|
||||
editor.documentSelection,
|
||||
Selection.single(path: [0], startOffset: 0),
|
||||
);
|
||||
|
||||
await editor.pressLogicKey(
|
||||
LogicalKeyboardKey.arrowRight,
|
||||
isMetaPressed: true,
|
||||
);
|
||||
expect(
|
||||
editor.documentSelection,
|
||||
Selection.single(path: [0], startOffset: text.length),
|
||||
);
|
||||
|
||||
await editor.pressLogicKey(
|
||||
LogicalKeyboardKey.arrowUp,
|
||||
isMetaPressed: true,
|
||||
);
|
||||
expect(
|
||||
editor.documentSelection,
|
||||
Selection.single(path: [0], startOffset: 0),
|
||||
);
|
||||
|
||||
await editor.pressLogicKey(
|
||||
LogicalKeyboardKey.arrowDown,
|
||||
isMetaPressed: true,
|
||||
);
|
||||
expect(
|
||||
editor.documentSelection,
|
||||
Selection.single(path: [1], startOffset: text.length),
|
||||
);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user