mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
Merge pull request #727 from AppFlowy-IO/feat/handle-arrow-keys
Feat/handle arrow keys
This commit is contained in:
commit
583d838344
@ -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
|
||||
|
@ -70,6 +70,11 @@ class _SelectedTextNodeWidgetState extends State<_SelectedTextNodeWidget>
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Offset localToGlobal(Offset offset) {
|
||||
return _renderParagraph.localToGlobal(offset);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Rect> getRectsInSelection(Selection selection) {
|
||||
assert(pathEquals(selection.start.path, selection.end.path));
|
||||
|
@ -33,7 +33,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;
|
||||
|
||||
@ -62,7 +77,7 @@ class EditorState {
|
||||
for (final op in transaction.operations) {
|
||||
_applyOperation(op);
|
||||
}
|
||||
cursorSelection = transaction.afterSelection;
|
||||
updateCursorSelection(transaction.afterSelection);
|
||||
|
||||
if (options.recordUndo) {
|
||||
final undoItem = undoManager.getUndoHistoryItem();
|
||||
|
@ -255,6 +255,10 @@ class _FlowyRichTextState extends State<FlowyRichText> with Selectable {
|
||||
return Rect.zero;
|
||||
}
|
||||
|
||||
Offset localToGlobal(Offset offset) {
|
||||
return _renderParagraph.localToGlobal(offset);
|
||||
}
|
||||
|
||||
TextSpan get _decorateTextSpanWithGlobalStyle => TextSpan(
|
||||
children: _textSpan.children
|
||||
?.whereType<TextSpan>()
|
||||
|
@ -17,10 +17,10 @@ class CursorWidget extends StatefulWidget {
|
||||
final LayerLink layerLink;
|
||||
|
||||
@override
|
||||
State<CursorWidget> createState() => _CursorWidgetState();
|
||||
State<CursorWidget> createState() => CursorWidgetState();
|
||||
}
|
||||
|
||||
class _CursorWidgetState extends State<CursorWidget> {
|
||||
class CursorWidgetState extends State<CursorWidget> {
|
||||
bool showCursor = true;
|
||||
late Timer timer;
|
||||
|
||||
@ -28,7 +28,17 @@ class _CursorWidgetState extends State<CursorWidget> {
|
||||
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<CursorWidget> {
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
/// force the cursor widget to show for a while
|
||||
show() {
|
||||
setState(() {
|
||||
showCursor = true;
|
||||
});
|
||||
timer.cancel();
|
||||
super.dispose();
|
||||
timer = _initTimer();
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -23,6 +23,8 @@ mixin Selectable<T extends StatefulWidget> on State<T> {
|
||||
Position getPositionInOffset(Offset start);
|
||||
Rect getCursorRectInPosition(Position position);
|
||||
|
||||
Offset localToGlobal(Offset offset);
|
||||
|
||||
Position start();
|
||||
Position end();
|
||||
|
||||
|
@ -1,14 +1,143 @@
|
||||
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';
|
||||
|
||||
FlowyKeyEventHandler arrowKeysHandler = (editorState, event) {
|
||||
if (event.logicalKey != LogicalKeyboardKey.arrowUp &&
|
||||
event.logicalKey != LogicalKeyboardKey.arrowDown &&
|
||||
event.logicalKey != LogicalKeyboardKey.arrowLeft &&
|
||||
event.logicalKey != LogicalKeyboardKey.arrowRight) {
|
||||
int _endOffsetOfNode(Node node) {
|
||||
if (node is TextNode) {
|
||||
return node.delta.length;
|
||||
}
|
||||
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) {
|
||||
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;
|
||||
}
|
||||
|
||||
FlowyKeyEventHandler 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: true));
|
||||
}
|
||||
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());
|
||||
}
|
||||
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,7 +1,7 @@
|
||||
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';
|
||||
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';
|
||||
@ -28,6 +28,10 @@ mixin FlowySelectionService<T extends StatefulWidget> on State<T> {
|
||||
///
|
||||
void clearSelection();
|
||||
|
||||
List<Rect> rects();
|
||||
|
||||
Position? hitTest(Offset? offset);
|
||||
|
||||
///
|
||||
List<Node> getNodesInSelection(Selection selection);
|
||||
|
||||
@ -50,7 +54,7 @@ mixin FlowySelectionService<T extends StatefulWidget> on State<T> {
|
||||
/// [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.
|
||||
@ -110,6 +114,8 @@ class _FlowySelectionState extends State<FlowySelection>
|
||||
/// Tap
|
||||
Offset? tapOffset;
|
||||
|
||||
final List<Rect> _rects = [];
|
||||
|
||||
EditorState get editorState => widget.editorState;
|
||||
|
||||
@override
|
||||
@ -171,8 +177,13 @@ class _FlowySelectionState extends State<FlowySelection>
|
||||
);
|
||||
}
|
||||
|
||||
List<Rect> rects() {
|
||||
return _rects;
|
||||
}
|
||||
|
||||
@override
|
||||
void updateSelection(Selection selection) {
|
||||
_rects.clear();
|
||||
_clearSelection();
|
||||
|
||||
// cursor
|
||||
@ -274,16 +285,32 @@ class _FlowySelectionState extends State<FlowySelection>
|
||||
|
||||
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);
|
||||
updateSelection(selection);
|
||||
}
|
||||
final position = hitTest(tapOffset);
|
||||
if (position == null) {
|
||||
return;
|
||||
}
|
||||
final selection = Selection.collapsed(position);
|
||||
editorState.updateCursorSelection(selection);
|
||||
}
|
||||
|
||||
@override
|
||||
Position? hitTest(Offset? offset) {
|
||||
if (offset == null) {
|
||||
editorState.updateCursorSelection(null);
|
||||
return null;
|
||||
}
|
||||
final nodes = getNodesInRange(offset);
|
||||
if (nodes.isEmpty) {
|
||||
editorState.updateCursorSelection(null);
|
||||
return null;
|
||||
}
|
||||
assert(nodes.length == 1);
|
||||
final selectable = nodes.first.selectable;
|
||||
if (selectable == null) {
|
||||
editorState.updateCursorSelection(null);
|
||||
return null;
|
||||
}
|
||||
return selectable.getPositionInOffset(offset);
|
||||
}
|
||||
|
||||
void _onPanStart(DragStartDetails details) {
|
||||
@ -314,7 +341,7 @@ class _FlowySelectionState extends State<FlowySelection>
|
||||
final selection = Selection(
|
||||
start: isDownward ? start : end, end: isDownward ? end : start);
|
||||
debugPrint('[_onPanUpdate] $selection');
|
||||
updateSelection(selection);
|
||||
editorState.updateCursorSelection(selection);
|
||||
}
|
||||
}
|
||||
|
||||
@ -385,6 +412,7 @@ class _FlowySelectionState extends State<FlowySelection>
|
||||
final rects = selectable.getRectsInSelection(newSelection);
|
||||
|
||||
for (final rect in rects) {
|
||||
_rects.add(_transformRectToGlobal(selectable, rect));
|
||||
final overlay = OverlayEntry(
|
||||
builder: ((context) => SelectionWidget(
|
||||
color: widget.selectionColor,
|
||||
@ -399,6 +427,11 @@ class _FlowySelectionState extends State<FlowySelection>
|
||||
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);
|
||||
|
||||
@ -413,6 +446,7 @@ class _FlowySelectionState extends State<FlowySelection>
|
||||
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,
|
||||
@ -423,9 +457,15 @@ class _FlowySelectionState extends State<FlowySelection>
|
||||
);
|
||||
_cursorOverlays.add(cursor);
|
||||
Overlay.of(context)?.insertAll(_cursorOverlays);
|
||||
_forceShowCursor();
|
||||
}
|
||||
}
|
||||
|
||||
_forceShowCursor() {
|
||||
final currentState = _cursorKey.currentState as CursorWidgetState?;
|
||||
currentState?.show();
|
||||
}
|
||||
|
||||
List<Node> _selectedNodesInSelection(Node node, Selection selection) {
|
||||
List<Node> result = [];
|
||||
if (node.parent != null) {
|
||||
|
@ -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
|
||||
#
|
||||
|
Loading…
Reference in New Issue
Block a user