mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
Merge pull request #722 from LucasXu0/feat/flowy_editor
chore: add selection_service documentation and resymbol shortcut service.
This commit is contained in:
@ -96,7 +96,7 @@ class _MyHomePageState extends State<MyHomePage> {
|
|||||||
);
|
);
|
||||||
return FlowyEditor(
|
return FlowyEditor(
|
||||||
editorState: _editorState,
|
editorState: _editorState,
|
||||||
keyEventHandler: const [],
|
keyEventHandlers: const [],
|
||||||
shortcuts: [
|
shortcuts: [
|
||||||
// TODO: this won't work, just a example for now.
|
// TODO: this won't work, just a example for now.
|
||||||
{
|
{
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import 'package:flowy_editor/document/position.dart';
|
||||||
|
import 'package:flowy_editor/document/selection.dart';
|
||||||
import 'package:flowy_editor/flowy_editor.dart';
|
import 'package:flowy_editor/flowy_editor.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
@ -38,37 +40,39 @@ class __ImageNodeWidgetState extends State<_ImageNodeWidget> with Selectable {
|
|||||||
String get src => widget.node.attributes['image_src'] as String;
|
String get src => widget.node.attributes['image_src'] as String;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Rect> getSelectionRectsInRange(Offset start, Offset end) {
|
Position end() {
|
||||||
final renderBox = context.findRenderObject() as RenderBox;
|
// TODO: implement end
|
||||||
return [Offset.zero & renderBox.size];
|
throw UnimplementedError();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Rect getCursorRect(Offset start) {
|
Position start() {
|
||||||
final renderBox = context.findRenderObject() as RenderBox;
|
// TODO: implement start
|
||||||
final size = Size(2, renderBox.size.height);
|
throw UnimplementedError();
|
||||||
final cursorOffset = Offset(renderBox.size.width, 0);
|
|
||||||
return cursorOffset & size;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
TextSelection? getCurrentTextSelection() {
|
List<Rect> getRectsInSelection(Selection selection) {
|
||||||
return null;
|
// TODO: implement getRectsInSelection
|
||||||
|
throw UnimplementedError();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Offset getOffsetByTextSelection(TextSelection textSelection) {
|
Selection getSelectionInRange(Offset start, Offset end) {
|
||||||
return Offset.zero;
|
// TODO: implement getSelectionInRange
|
||||||
|
throw UnimplementedError();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Offset getBackwardOffset() {
|
Rect getCursorRectInPosition(Position position) {
|
||||||
return Offset.zero;
|
// TODO: implement getCursorRectInPosition
|
||||||
|
throw UnimplementedError();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Offset getForwardOffset() {
|
Position getPositionInOffset(Offset start) {
|
||||||
return Offset.zero;
|
// TODO: implement getPositionInOffset
|
||||||
|
throw UnimplementedError();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:example/plugin/debuggable_rich_text.dart';
|
import 'package:example/plugin/debuggable_rich_text.dart';
|
||||||
|
import 'package:flowy_editor/document/selection.dart';
|
||||||
|
import 'package:flowy_editor/document/position.dart';
|
||||||
import 'package:flowy_editor/flowy_editor.dart';
|
import 'package:flowy_editor/flowy_editor.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/gestures.dart';
|
import 'package:flutter/gestures.dart';
|
||||||
@ -56,49 +58,43 @@ class _SelectedTextNodeWidgetState extends State<_SelectedTextNodeWidget>
|
|||||||
_textKey.currentContext?.findRenderObject() as RenderParagraph;
|
_textKey.currentContext?.findRenderObject() as RenderParagraph;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Rect> getSelectionRectsInRange(Offset start, Offset end) {
|
Selection getSelectionInRange(Offset start, Offset end) {
|
||||||
final localStart = _renderParagraph.globalToLocal(start);
|
final localStart = _renderParagraph.globalToLocal(start);
|
||||||
final localEnd = _renderParagraph.globalToLocal(end);
|
final localEnd = _renderParagraph.globalToLocal(end);
|
||||||
|
final baseOffset = _getTextPositionAtOffset(localStart).offset;
|
||||||
var textSelection =
|
final extentOffset = _getTextPositionAtOffset(localEnd).offset;
|
||||||
TextSelection(baseOffset: 0, extentOffset: node.toRawString().length);
|
return Selection.single(
|
||||||
// Returns select all if the start or end exceeds the size of the box
|
path: node.path,
|
||||||
// TODO: don't need to compute everytime.
|
startOffset: baseOffset,
|
||||||
var rects = _computeSelectionRects(textSelection);
|
endOffset: extentOffset,
|
||||||
_textSelection = textSelection;
|
);
|
||||||
|
}
|
||||||
if (localEnd.dy > localStart.dy) {
|
|
||||||
// downward
|
@override
|
||||||
if (localEnd.dy >= rects.last.bottom) {
|
List<Rect> getRectsInSelection(Selection selection) {
|
||||||
return rects;
|
assert(pathEquals(selection.start.path, selection.end.path));
|
||||||
}
|
assert(pathEquals(selection.start.path, node.path));
|
||||||
} else {
|
final textSelection = TextSelection(
|
||||||
// upward
|
baseOffset: selection.start.offset,
|
||||||
if (localEnd.dy <= rects.first.top) {
|
extentOffset: selection.end.offset,
|
||||||
return rects;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final selectionBaseOffset = _getTextPositionAtOffset(localStart).offset;
|
|
||||||
final selectionExtentOffset = _getTextPositionAtOffset(localEnd).offset;
|
|
||||||
textSelection = TextSelection(
|
|
||||||
baseOffset: selectionBaseOffset,
|
|
||||||
extentOffset: selectionExtentOffset,
|
|
||||||
);
|
);
|
||||||
_textSelection = textSelection;
|
|
||||||
return _computeSelectionRects(textSelection);
|
return _computeSelectionRects(textSelection);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Rect getCursorRect(Offset start) {
|
Rect getCursorRectInPosition(Position position) {
|
||||||
final localStart = _renderParagraph.globalToLocal(start);
|
final textSelection = TextSelection.collapsed(offset: position.offset);
|
||||||
final selectionBaseOffset = _getTextPositionAtOffset(localStart).offset;
|
|
||||||
final textSelection = TextSelection.collapsed(offset: selectionBaseOffset);
|
|
||||||
_textSelection = textSelection;
|
_textSelection = textSelection;
|
||||||
print('text selection = $textSelection');
|
|
||||||
return _computeCursorRect(textSelection.baseOffset);
|
return _computeCursorRect(textSelection.baseOffset);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Position getPositionInOffset(Offset start) {
|
||||||
|
final localStart = _renderParagraph.globalToLocal(start);
|
||||||
|
final baseOffset = _getTextPositionAtOffset(localStart).offset;
|
||||||
|
return Position(path: node.path, offset: baseOffset);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
TextSelection? getCurrentTextSelection() {
|
TextSelection? getCurrentTextSelection() {
|
||||||
return _textSelection;
|
return _textSelection;
|
||||||
@ -111,28 +107,11 @@ class _SelectedTextNodeWidgetState extends State<_SelectedTextNodeWidget>
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Offset getBackwardOffset() {
|
Position start() => Position(path: node.path, offset: 0);
|
||||||
final textSelection = _textSelection;
|
|
||||||
if (textSelection != null) {
|
|
||||||
final leftTextSelection = TextSelection.collapsed(
|
|
||||||
offset: max(0, textSelection.baseOffset - 1),
|
|
||||||
);
|
|
||||||
return getOffsetByTextSelection(leftTextSelection);
|
|
||||||
}
|
|
||||||
return Offset.zero;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Offset getForwardOffset() {
|
Position end() =>
|
||||||
final textSelection = _textSelection;
|
Position(path: node.path, offset: node.toRawString().length);
|
||||||
if (textSelection != null) {
|
|
||||||
final leftTextSelection = TextSelection.collapsed(
|
|
||||||
offset: min(node.toRawString().length, textSelection.extentOffset + 1),
|
|
||||||
);
|
|
||||||
return getOffsetByTextSelection(leftTextSelection);
|
|
||||||
}
|
|
||||||
return Offset.zero;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -175,8 +154,8 @@ class _SelectedTextNodeWidgetState extends State<_SelectedTextNodeWidget>
|
|||||||
return _renderParagraph.getPositionForOffset(offset);
|
return _renderParagraph.getPositionForOffset(offset);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Rect> _computeSelectionRects(TextSelection selection) {
|
List<Rect> _computeSelectionRects(TextSelection textSelection) {
|
||||||
final textBoxes = _renderParagraph.getBoxesForSelection(selection);
|
final textBoxes = _renderParagraph.getBoxesForSelection(textSelection);
|
||||||
return textBoxes.map((box) => box.toRect()).toList();
|
return textBoxes.map((box) => box.toRect()).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -185,7 +164,6 @@ class _SelectedTextNodeWidgetState extends State<_SelectedTextNodeWidget>
|
|||||||
final cursorOffset =
|
final cursorOffset =
|
||||||
_renderParagraph.getOffsetForCaret(position, Rect.zero);
|
_renderParagraph.getOffsetForCaret(position, Rect.zero);
|
||||||
final cursorHeight = _renderParagraph.getFullHeightForCaret(position);
|
final cursorHeight = _renderParagraph.getFullHeightForCaret(position);
|
||||||
print('offset = $offset, cursorHeight = $cursorHeight');
|
|
||||||
if (cursorHeight != null) {
|
if (cursorHeight != null) {
|
||||||
const cursorWidth = 2;
|
const cursorWidth = 2;
|
||||||
return Rect.fromLTWH(
|
return Rect.fromLTWH(
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import 'package:flowy_editor/document/position.dart';
|
import 'package:flowy_editor/document/position.dart';
|
||||||
import 'package:flowy_editor/document/selection.dart';
|
import 'package:flowy_editor/document/selection.dart';
|
||||||
import 'package:flowy_editor/document/text_delta.dart';
|
|
||||||
import 'package:flutter/gestures.dart';
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flowy_editor/flowy_editor.dart';
|
import 'package:flowy_editor/flowy_editor.dart';
|
||||||
@ -327,7 +326,7 @@ TextSelection? _globalSelectionToLocal(Node node, Selection? globalSel) {
|
|||||||
if (!pathEquals(nodePath, globalSel.start.path)) {
|
if (!pathEquals(nodePath, globalSel.start.path)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (globalSel.isCollapsed()) {
|
if (globalSel.isCollapsed) {
|
||||||
return TextSelection(
|
return TextSelection(
|
||||||
baseOffset: globalSel.start.offset, extentOffset: globalSel.end.offset);
|
baseOffset: globalSel.start.offset, extentOffset: globalSel.end.offset);
|
||||||
} else {
|
} else {
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
typedef Path = List<int>;
|
typedef Path = List<int>;
|
||||||
|
@ -24,4 +24,14 @@ class Position {
|
|||||||
final pathHash = hashList(path);
|
final pathHash = hashList(path);
|
||||||
return Object.hash(pathHash, offset);
|
return Object.hash(pathHash, offset);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Position copyWith({Path? path, int? offset}) {
|
||||||
|
return Position(
|
||||||
|
path: path ?? this.path,
|
||||||
|
offset: offset ?? this.offset,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'path = $path, offset = $offset';
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
import './position.dart';
|
import 'package:flowy_editor/document/path.dart';
|
||||||
|
import 'package:flowy_editor/document/position.dart';
|
||||||
|
import 'package:flowy_editor/extensions/path_extensions.dart';
|
||||||
|
|
||||||
class Selection {
|
class Selection {
|
||||||
final Position start;
|
final Position start;
|
||||||
@ -9,9 +11,16 @@ class Selection {
|
|||||||
required this.end,
|
required this.end,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory Selection.collapsed(Position pos) {
|
Selection.single({
|
||||||
return Selection(start: pos, end: pos);
|
required Path path,
|
||||||
}
|
required int startOffset,
|
||||||
|
int? endOffset,
|
||||||
|
}) : start = Position(path: path, offset: startOffset),
|
||||||
|
end = Position(path: path, offset: endOffset ?? startOffset);
|
||||||
|
|
||||||
|
Selection.collapsed(Position position)
|
||||||
|
: start = position,
|
||||||
|
end = position;
|
||||||
|
|
||||||
Selection collapse({bool atStart = false}) {
|
Selection collapse({bool atStart = false}) {
|
||||||
if (atStart) {
|
if (atStart) {
|
||||||
@ -21,7 +30,22 @@ class Selection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isCollapsed() {
|
bool get isCollapsed => start == end;
|
||||||
return 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 =>
|
||||||
|
start.path <= end.path && !pathEquals(start.path, end.path);
|
||||||
|
|
||||||
|
Selection copyWith({Position? start, Position? end}) {
|
||||||
|
return Selection(
|
||||||
|
start: start ?? this.start,
|
||||||
|
end: end ?? this.end,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Selection copy() => Selection(start: start, end: end);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => '[Selection] start = $start, end = $end';
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,23 @@
|
|||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:flowy_editor/document/node.dart';
|
||||||
|
import 'package:flowy_editor/document/selection.dart';
|
||||||
|
import 'package:flowy_editor/extensions/object_extensions.dart';
|
||||||
|
import 'package:flowy_editor/extensions/path_extensions.dart';
|
||||||
|
import 'package:flowy_editor/render/selection/selectable.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
extension NodeExtensions on Node {
|
||||||
|
RenderBox? get renderBox =>
|
||||||
|
key?.currentContext?.findRenderObject()?.unwrapOrNull<RenderBox>();
|
||||||
|
|
||||||
|
Selectable? get selectable => key?.currentState?.unwrapOrNull<Selectable>();
|
||||||
|
|
||||||
|
bool inSelection(Selection selection) {
|
||||||
|
if (selection.start.path <= selection.end.path) {
|
||||||
|
return selection.start.path <= path && path <= selection.end.path;
|
||||||
|
} else {
|
||||||
|
return selection.end.path <= path && path <= selection.start.path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
import 'package:flowy_editor/document/path.dart';
|
||||||
|
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
extension PathExtensions on Path {
|
||||||
|
bool operator >=(Path other) {
|
||||||
|
final length = min(this.length, other.length);
|
||||||
|
for (var i = 0; i < length; i++) {
|
||||||
|
if (this[i] < other[i]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool operator <=(Path other) {
|
||||||
|
final length = min(this.length, other.length);
|
||||||
|
for (var i = 0; i < length; i++) {
|
||||||
|
if (this[i] > other[i]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
@ -1,3 +1,5 @@
|
|||||||
|
import 'package:flowy_editor/document/position.dart';
|
||||||
|
import 'package:flowy_editor/document/selection.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
///
|
///
|
||||||
@ -9,20 +11,20 @@ mixin Selectable<T extends StatefulWidget> on State<T> {
|
|||||||
///
|
///
|
||||||
/// The return result must be a [List] of the [Rect]
|
/// The return result must be a [List] of the [Rect]
|
||||||
/// under the local coordinate system.
|
/// under the local coordinate system.
|
||||||
List<Rect> getSelectionRectsInRange(Offset start, Offset end);
|
Selection getSelectionInRange(Offset start, Offset end);
|
||||||
|
|
||||||
|
List<Rect> getRectsInSelection(Selection selection);
|
||||||
|
|
||||||
/// Returns a [Rect] for the offset in current widget.
|
/// Returns a [Rect] for the offset in current widget.
|
||||||
///
|
///
|
||||||
/// [start] is the offset of the global coordination system.
|
/// [start] is the offset of the global coordination system.
|
||||||
///
|
///
|
||||||
/// The return result must be an offset of the local coordinate system.
|
/// The return result must be an offset of the local coordinate system.
|
||||||
Rect getCursorRect(Offset start);
|
Position getPositionInOffset(Offset start);
|
||||||
|
Rect getCursorRectInPosition(Position position);
|
||||||
|
|
||||||
/// Returns a backward offset of the current offset based on the cause.
|
Position start();
|
||||||
Offset getBackwardOffset(/* Cause */);
|
Position end();
|
||||||
|
|
||||||
/// Returns a forward offset of the current offset based on the cause.
|
|
||||||
Offset getForwardOffset(/* Cause */);
|
|
||||||
|
|
||||||
/// For [TextNode] only.
|
/// For [TextNode] only.
|
||||||
///
|
///
|
||||||
@ -30,12 +32,12 @@ mixin Selectable<T extends StatefulWidget> on State<T> {
|
|||||||
///
|
///
|
||||||
/// Only the widget rendered by [TextNode] need to implement the detail,
|
/// Only the widget rendered by [TextNode] need to implement the detail,
|
||||||
/// and the rest can return null.
|
/// and the rest can return null.
|
||||||
TextSelection? getCurrentTextSelection();
|
TextSelection? getCurrentTextSelection() => null;
|
||||||
|
|
||||||
/// For [TextNode] only.
|
/// For [TextNode] only.
|
||||||
///
|
///
|
||||||
/// Retruns a [Offset].
|
/// Retruns a [Offset].
|
||||||
/// Only the widget rendered by [TextNode] need to implement the detail,
|
/// Only the widget rendered by [TextNode] need to implement the detail,
|
||||||
/// and the rest can return [Offset.zero].
|
/// and the rest can return [Offset.zero].
|
||||||
Offset getOffsetByTextSelection(TextSelection textSelection);
|
Offset getOffsetByTextSelection(TextSelection textSelection) => Offset.zero;
|
||||||
}
|
}
|
||||||
|
@ -1,25 +1,27 @@
|
|||||||
import 'package:flowy_editor/render/selection/floating_shortcut_widget.dart';
|
import 'package:flowy_editor/render/selection/floating_shortcut_widget.dart';
|
||||||
import 'package:flowy_editor/service/floating_shortcut_service.dart';
|
import 'package:flowy_editor/service/shortcut_service.dart';
|
||||||
import 'package:flowy_editor/service/flowy_key_event_handlers/arrow_keys_handler.dart';
|
import 'package:flowy_editor/service/internal_key_event_handlers/arrow_keys_handler.dart';
|
||||||
import 'package:flowy_editor/service/flowy_key_event_handlers/delete_nodes_handler.dart';
|
import 'package:flowy_editor/service/internal_key_event_handlers/delete_nodes_handler.dart';
|
||||||
import 'package:flowy_editor/service/flowy_key_event_handlers/delete_single_text_node_handler.dart';
|
import 'package:flowy_editor/service/internal_key_event_handlers/delete_single_text_node_handler.dart';
|
||||||
import 'package:flowy_editor/service/flowy_key_event_handlers/shortcut_handler.dart';
|
import 'package:flowy_editor/service/internal_key_event_handlers/shortcut_handler.dart';
|
||||||
import 'package:flowy_editor/service/keyboard_service.dart';
|
import 'package:flowy_editor/service/keyboard_service.dart';
|
||||||
import 'package:flowy_editor/service/selection_service.dart';
|
import 'package:flowy_editor/service/selection_service.dart';
|
||||||
|
import 'package:flowy_editor/editor_state.dart';
|
||||||
|
|
||||||
import '../editor_state.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class FlowyEditor extends StatefulWidget {
|
class FlowyEditor extends StatefulWidget {
|
||||||
const FlowyEditor({
|
const FlowyEditor({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.editorState,
|
required this.editorState,
|
||||||
required this.keyEventHandler,
|
required this.keyEventHandlers,
|
||||||
required this.shortcuts,
|
required this.shortcuts,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
final EditorState editorState;
|
final EditorState editorState;
|
||||||
final List<FlowyKeyEventHandler> keyEventHandler;
|
final List<FlowyKeyEventHandler> keyEventHandlers;
|
||||||
|
|
||||||
|
/// Shortcusts
|
||||||
final FloatingShortcuts shortcuts;
|
final FloatingShortcuts shortcuts;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -41,7 +43,7 @@ class _FlowyEditorState extends State<FlowyEditor> {
|
|||||||
flowyDeleteNodesHandler,
|
flowyDeleteNodesHandler,
|
||||||
deleteSingleTextNodeHandler,
|
deleteSingleTextNodeHandler,
|
||||||
arrowKeysHandler,
|
arrowKeysHandler,
|
||||||
...widget.keyEventHandler,
|
...widget.keyEventHandlers,
|
||||||
],
|
],
|
||||||
editorState: editorState,
|
editorState: editorState,
|
||||||
child: FloatingShortcut(
|
child: FloatingShortcut(
|
||||||
|
@ -1,37 +0,0 @@
|
|||||||
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';
|
|
||||||
|
|
||||||
FlowyKeyEventHandler arrowKeysHandler = (editorState, event) {
|
|
||||||
if (event.logicalKey != LogicalKeyboardKey.arrowUp &&
|
|
||||||
event.logicalKey != LogicalKeyboardKey.arrowDown &&
|
|
||||||
event.logicalKey != LogicalKeyboardKey.arrowLeft &&
|
|
||||||
event.logicalKey != LogicalKeyboardKey.arrowRight) {
|
|
||||||
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<TextNode>();
|
|
||||||
final selectable = node?.key?.currentState?.unwrapOrNull<Selectable>();
|
|
||||||
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;
|
|
||||||
};
|
|
@ -0,0 +1,14 @@
|
|||||||
|
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) {
|
||||||
|
return KeyEventResult.ignored;
|
||||||
|
}
|
||||||
|
|
||||||
|
return KeyEventResult.ignored;
|
||||||
|
};
|
@ -37,7 +37,7 @@ FlowyKeyEventHandler deleteSingleTextNodeHandler = (editorState, event) {
|
|||||||
final newOfset = previousSelectable
|
final newOfset = previousSelectable
|
||||||
?.getOffsetByTextSelection(newTextSelection);
|
?.getOffsetByTextSelection(newTextSelection);
|
||||||
if (newOfset != null) {
|
if (newOfset != null) {
|
||||||
selectionService.updateCursor(newOfset);
|
// selectionService.updateCursor(newOfset);
|
||||||
}
|
}
|
||||||
// merge
|
// merge
|
||||||
TransactionBuilder(editorState)
|
TransactionBuilder(editorState)
|
||||||
@ -58,7 +58,7 @@ FlowyKeyEventHandler deleteSingleTextNodeHandler = (editorState, event) {
|
|||||||
final selectionService = editorState.service.selectionService;
|
final selectionService = editorState.service.selectionService;
|
||||||
final newOfset =
|
final newOfset =
|
||||||
selectable.getOffsetByTextSelection(newTextSelection);
|
selectable.getOffsetByTextSelection(newTextSelection);
|
||||||
selectionService.updateCursor(newOfset);
|
// selectionService.updateCursor(newOfset);
|
||||||
return KeyEventResult.handled;
|
return KeyEventResult.handled;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -18,13 +18,13 @@ FlowyKeyEventHandler slashShortcutHandler = (editorState, event) {
|
|||||||
final textNode = selectedNodes.first.unwrapOrNull<TextNode>();
|
final textNode = selectedNodes.first.unwrapOrNull<TextNode>();
|
||||||
final selectable = textNode?.key?.currentState?.unwrapOrNull<Selectable>();
|
final selectable = textNode?.key?.currentState?.unwrapOrNull<Selectable>();
|
||||||
final textSelection = selectable?.getCurrentTextSelection();
|
final textSelection = selectable?.getCurrentTextSelection();
|
||||||
if (textNode != null && selectable != null && textSelection != null) {
|
// if (textNode != null && selectable != null && textSelection != null) {
|
||||||
final offset = selectable.getOffsetByTextSelection(textSelection);
|
// final offset = selectable.getOffsetByTextSelection(textSelection);
|
||||||
final rect = selectable.getCursorRect(offset);
|
// final rect = selectable.getCursorRect(offset);
|
||||||
editorState.service.floatingToolbarService
|
// editorState.service.floatingToolbarService
|
||||||
.showInOffset(rect.topLeft, textNode.layerLink);
|
// .showInOffset(rect.topLeft, textNode.layerLink);
|
||||||
return KeyEventResult.handled;
|
// return KeyEventResult.handled;
|
||||||
}
|
// }
|
||||||
|
|
||||||
return KeyEventResult.ignored;
|
return KeyEventResult.ignored;
|
||||||
};
|
};
|
@ -1,68 +1,94 @@
|
|||||||
|
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/cursor_widget.dart';
|
import 'package:flowy_editor/render/selection/cursor_widget.dart';
|
||||||
import 'package:flowy_editor/render/selection/flowy_selection_widget.dart';
|
import 'package:flowy_editor/render/selection/flowy_selection_widget.dart';
|
||||||
import 'package:flowy_editor/extensions/object_extensions.dart';
|
import 'package:flowy_editor/extensions/object_extensions.dart';
|
||||||
import 'package:flowy_editor/service/floating_shortcut_service.dart';
|
import 'package:flowy_editor/extensions/node_extensions.dart';
|
||||||
|
import 'package:flowy_editor/service/shortcut_service.dart';
|
||||||
|
import 'package:flowy_editor/editor_state.dart';
|
||||||
|
|
||||||
import 'package:flutter/gestures.dart';
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import '../editor_state.dart';
|
|
||||||
import '../document/node.dart';
|
|
||||||
import '../render/selection/selectable.dart';
|
|
||||||
|
|
||||||
/// Process selection and cursor
|
/// Process selection and cursor
|
||||||
mixin FlowySelectionService<T extends StatefulWidget> on State<T> {
|
mixin FlowySelectionService<T extends StatefulWidget> on State<T> {
|
||||||
/// [Pan] and [Tap] must be mutually exclusive.
|
/// Returns the currently selected [Node]s.
|
||||||
/// Pan
|
///
|
||||||
Offset? panStartOffset;
|
/// The order of the return is determined according to the selected order.
|
||||||
Offset? panEndOffset;
|
List<Node> get currentSelectedNodes;
|
||||||
|
|
||||||
/// Tap
|
/// ------------------ Selection ------------------------
|
||||||
Offset? tapOffset;
|
|
||||||
|
|
||||||
void updateSelection(Offset start, Offset end);
|
///
|
||||||
|
void updateSelection(Selection selection);
|
||||||
|
|
||||||
void updateCursor(Offset start);
|
///
|
||||||
|
void clearSelection();
|
||||||
|
|
||||||
/// Returns selected node(s)
|
///
|
||||||
/// Returns empty list if no nodes are being selected.
|
List<Node> getNodesInSelection(Selection selection);
|
||||||
List<Node> getSelectedNodes(Offset start, [Offset? end]);
|
|
||||||
|
|
||||||
/// Compute selected node triggered by [Tap]
|
/// ------------------ Selection ------------------------
|
||||||
Node? computeSelectedNodeInOffset(
|
|
||||||
Node node,
|
|
||||||
Offset offset,
|
|
||||||
);
|
|
||||||
|
|
||||||
/// Compute selected nodes triggered by [Pan]
|
/// ------------------ Offset ------------------------
|
||||||
List<Node> computeSelectedNodesInRange(
|
|
||||||
|
/// Returns selected [Node]s. Empty list would be returned
|
||||||
|
/// if no nodes are being selected.
|
||||||
|
///
|
||||||
|
///
|
||||||
|
/// [start] and [end] are the offsets under the global coordinate system.
|
||||||
|
///
|
||||||
|
/// If end is not null, it means multiple selection,
|
||||||
|
/// otherwise single selection.
|
||||||
|
List<Node> getNodesInRange(Offset start, [Offset? end]);
|
||||||
|
|
||||||
|
/// Return the [Node] or [Null] in single selection.
|
||||||
|
///
|
||||||
|
/// [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
|
||||||
|
/// if no nodes are in range.
|
||||||
|
///
|
||||||
|
/// [start] is the offset under the global coordinate system.
|
||||||
|
List<Node> computeNodesInRange(
|
||||||
Node node,
|
Node node,
|
||||||
Offset start,
|
Offset start,
|
||||||
Offset end,
|
Offset end,
|
||||||
);
|
);
|
||||||
|
|
||||||
/// Pan
|
/// Return [bool] to identify the [Node] is in Range or not.
|
||||||
bool isNodeInSelection(
|
///
|
||||||
|
/// [start] and [end] are the offsets under the global coordinate system.
|
||||||
|
bool isNodeInRange(
|
||||||
Node node,
|
Node node,
|
||||||
Offset start,
|
Offset start,
|
||||||
Offset end,
|
Offset end,
|
||||||
);
|
);
|
||||||
|
|
||||||
/// Tap
|
/// Return [bool] to identify the [Node] contains [Offset] or not.
|
||||||
bool isNodeInOffset(
|
///
|
||||||
Node node,
|
/// [start] is the offset under the global coordinate system.
|
||||||
Offset offset,
|
bool isNodeInOffset(Node node, Offset offset);
|
||||||
);
|
|
||||||
|
/// ------------------ Offset ------------------------
|
||||||
}
|
}
|
||||||
|
|
||||||
class FlowySelection extends StatefulWidget {
|
class FlowySelection extends StatefulWidget {
|
||||||
const FlowySelection({
|
const FlowySelection({
|
||||||
Key? key,
|
Key? key,
|
||||||
|
this.cursorColor = Colors.black,
|
||||||
|
this.selectionColor = const Color.fromARGB(60, 61, 61, 213),
|
||||||
required this.editorState,
|
required this.editorState,
|
||||||
required this.child,
|
required this.child,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
final EditorState editorState;
|
final EditorState editorState;
|
||||||
final Widget child;
|
final Widget child;
|
||||||
|
final Color cursorColor;
|
||||||
|
final Color selectionColor;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<FlowySelection> createState() => _FlowySelectionState();
|
State<FlowySelection> createState() => _FlowySelectionState();
|
||||||
@ -75,8 +101,23 @@ class _FlowySelectionState extends State<FlowySelection>
|
|||||||
final List<OverlayEntry> _selectionOverlays = [];
|
final List<OverlayEntry> _selectionOverlays = [];
|
||||||
final List<OverlayEntry> _cursorOverlays = [];
|
final List<OverlayEntry> _cursorOverlays = [];
|
||||||
|
|
||||||
|
/// [Pan] and [Tap] must be mutually exclusive.
|
||||||
|
/// Pan
|
||||||
|
Offset? panStartOffset;
|
||||||
|
Offset? panEndOffset;
|
||||||
|
|
||||||
|
/// Tap
|
||||||
|
Offset? tapOffset;
|
||||||
|
|
||||||
EditorState get editorState => widget.editorState;
|
EditorState get editorState => widget.editorState;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Node> currentSelectedNodes = [];
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Node> getNodesInSelection(Selection selection) =>
|
||||||
|
_selectedNodesInSelection(editorState.document.root, selection);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return RawGestureDetector(
|
return RawGestureDetector(
|
||||||
@ -105,76 +146,28 @@ class _FlowySelectionState extends State<FlowySelection>
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void updateSelection(Offset start, Offset end) {
|
void updateSelection(Selection selection) {
|
||||||
_clearAllOverlayEntries();
|
_clearSelection();
|
||||||
|
|
||||||
final nodes = getSelectedNodes(start, end);
|
// cursor
|
||||||
editorState.selectedNodes = nodes;
|
if (selection.isCollapsed) {
|
||||||
if (nodes.isEmpty) {
|
_updateCursor(selection.start);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (final node in nodes) {
|
|
||||||
if (node.key?.currentState is! Selectable) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
final selectable = node.key?.currentState as Selectable;
|
|
||||||
final selectionRects = selectable.getSelectionRectsInRange(start, end);
|
|
||||||
for (final rect in selectionRects) {
|
|
||||||
final overlay = OverlayEntry(
|
|
||||||
builder: ((context) => SelectionWidget(
|
|
||||||
color: Colors.yellow.withAlpha(100),
|
|
||||||
layerLink: node.layerLink,
|
|
||||||
rect: rect,
|
|
||||||
)),
|
|
||||||
);
|
|
||||||
_selectionOverlays.add(overlay);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Overlay.of(context)?.insertAll(_selectionOverlays);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void updateCursor(Offset start) {
|
|
||||||
_clearAllOverlayEntries();
|
|
||||||
|
|
||||||
final nodes = getSelectedNodes(start);
|
|
||||||
editorState.selectedNodes = nodes;
|
|
||||||
if (nodes.isEmpty) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final selectedNode = nodes.first;
|
|
||||||
if (selectedNode.key?.currentState is! Selectable) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final selectable = selectedNode.key?.currentState as Selectable;
|
|
||||||
final rect = selectable.getCursorRect(start);
|
|
||||||
final cursor = OverlayEntry(
|
|
||||||
builder: ((context) => CursorWidget(
|
|
||||||
key: _cursorKey,
|
|
||||||
rect: rect,
|
|
||||||
color: Colors.red,
|
|
||||||
layerLink: selectedNode.layerLink,
|
|
||||||
)),
|
|
||||||
);
|
|
||||||
_cursorOverlays.add(cursor);
|
|
||||||
Overlay.of(context)?.insertAll(_cursorOverlays);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<Node> getSelectedNodes(Offset start, [Offset? end]) {
|
|
||||||
if (end != null) {
|
|
||||||
return computeSelectedNodesInRange(
|
|
||||||
editorState.document.root,
|
|
||||||
start,
|
|
||||||
end,
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
final reuslt = computeSelectedNodeInOffset(
|
_updateSelection(selection);
|
||||||
editorState.document.root,
|
}
|
||||||
start,
|
}
|
||||||
);
|
|
||||||
|
@override
|
||||||
|
void clearSelection() {
|
||||||
|
_clearSelection();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Node> getNodesInRange(Offset start, [Offset? end]) {
|
||||||
|
if (end != null) {
|
||||||
|
return computeNodesInRange(editorState.document.root, start, end);
|
||||||
|
} else {
|
||||||
|
final reuslt = computeNodeInOffset(editorState.document.root, start);
|
||||||
if (reuslt != null) {
|
if (reuslt != null) {
|
||||||
return [reuslt];
|
return [reuslt];
|
||||||
}
|
}
|
||||||
@ -183,43 +176,49 @@ class _FlowySelectionState extends State<FlowySelection>
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Node? computeSelectedNodeInOffset(Node node, Offset offset) {
|
Node? computeNodeInOffset(Node node, Offset offset) {
|
||||||
for (final child in node.children) {
|
for (final child in node.children) {
|
||||||
final result = computeSelectedNodeInOffset(child, offset);
|
final result = computeNodeInOffset(child, offset);
|
||||||
if (result != null) {
|
if (result != null) {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (node.parent != null && node.key != null) {
|
if (node.parent != null && node.key != null) {
|
||||||
if (isNodeInOffset(node, offset)) {
|
if (isNodeInOffset(node, offset)) {
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Node> computeSelectedNodesInRange(Node node, Offset start, Offset end) {
|
List<Node> 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<Node> _computeNodesInRange(Node node, Offset start, Offset end) {
|
||||||
List<Node> result = [];
|
List<Node> result = [];
|
||||||
if (node.parent != null && node.key != null) {
|
if (node.parent != null && node.key != null) {
|
||||||
if (isNodeInSelection(node, start, end)) {
|
if (isNodeInRange(node, start, end)) {
|
||||||
result.add(node);
|
result.add(node);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (final child in node.children) {
|
for (final child in node.children) {
|
||||||
result.addAll(computeSelectedNodesInRange(child, start, end));
|
result.addAll(computeNodesInRange(child, start, end));
|
||||||
}
|
}
|
||||||
// TODO: sort the result
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool isNodeInOffset(Node node, Offset offset) {
|
bool isNodeInOffset(Node node, Offset offset) {
|
||||||
assert(node.key != null);
|
final renderBox = node.renderBox;
|
||||||
final renderBox =
|
|
||||||
node.key?.currentContext?.findRenderObject() as RenderBox?;
|
|
||||||
if (renderBox != null) {
|
if (renderBox != null) {
|
||||||
final boxOffset = renderBox.localToGlobal(Offset.zero);
|
final boxOffset = renderBox.localToGlobal(Offset.zero);
|
||||||
final boxRect = boxOffset & renderBox.size;
|
final boxRect = boxOffset & renderBox.size;
|
||||||
@ -229,10 +228,8 @@ class _FlowySelectionState extends State<FlowySelection>
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool isNodeInSelection(Node node, Offset start, Offset end) {
|
bool isNodeInRange(Node node, Offset start, Offset end) {
|
||||||
assert(node.key != null);
|
final renderBox = node.renderBox;
|
||||||
final renderBox =
|
|
||||||
node.key?.currentContext?.findRenderObject() as RenderBox?;
|
|
||||||
if (renderBox != null) {
|
if (renderBox != null) {
|
||||||
final rect = Rect.fromPoints(start, end);
|
final rect = Rect.fromPoints(start, end);
|
||||||
final boxOffset = renderBox.localToGlobal(Offset.zero);
|
final boxOffset = renderBox.localToGlobal(Offset.zero);
|
||||||
@ -243,59 +240,168 @@ class _FlowySelectionState extends State<FlowySelection>
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _onTapDown(TapDownDetails details) {
|
void _onTapDown(TapDownDetails details) {
|
||||||
debugPrint('on tap down');
|
// clear old state.
|
||||||
|
|
||||||
// TODO: use setter to make them exclusive??
|
|
||||||
tapOffset = details.globalPosition;
|
|
||||||
panStartOffset = null;
|
panStartOffset = null;
|
||||||
panEndOffset = null;
|
panEndOffset = null;
|
||||||
|
|
||||||
updateCursor(tapOffset!);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onPanStart(DragStartDetails details) {
|
void _onPanStart(DragStartDetails details) {
|
||||||
debugPrint('on pan start');
|
// clear old state.
|
||||||
|
|
||||||
panStartOffset = details.globalPosition;
|
|
||||||
panEndOffset = null;
|
panEndOffset = null;
|
||||||
tapOffset = null;
|
tapOffset = null;
|
||||||
|
clearSelection();
|
||||||
|
|
||||||
|
panStartOffset = details.globalPosition;
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onPanUpdate(DragUpdateDetails details) {
|
void _onPanUpdate(DragUpdateDetails details) {
|
||||||
// debugPrint('on pan update');
|
|
||||||
|
|
||||||
panEndOffset = details.globalPosition;
|
panEndOffset = details.globalPosition;
|
||||||
tapOffset = null;
|
|
||||||
|
|
||||||
updateSelection(panStartOffset!, panEndOffset!);
|
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) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onPanEnd(DragEndDetails details) {
|
void _onPanEnd(DragEndDetails details) {
|
||||||
// do nothing
|
// do nothing
|
||||||
}
|
}
|
||||||
|
|
||||||
void _clearAllOverlayEntries() {
|
|
||||||
_clearSelection();
|
|
||||||
_clearCursor();
|
|
||||||
_clearFloatingShorts();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _clearSelection() {
|
void _clearSelection() {
|
||||||
|
currentSelectedNodes = [];
|
||||||
|
|
||||||
|
// clear selection
|
||||||
_selectionOverlays
|
_selectionOverlays
|
||||||
..forEach((overlay) => overlay.remove())
|
..forEach((overlay) => overlay.remove())
|
||||||
..clear();
|
..clear();
|
||||||
}
|
// clear cursors
|
||||||
|
|
||||||
void _clearCursor() {
|
|
||||||
_cursorOverlays
|
_cursorOverlays
|
||||||
..forEach((overlay) => overlay.remove())
|
..forEach((overlay) => overlay.remove())
|
||||||
..clear();
|
..clear();
|
||||||
|
// clear floating shortcusts
|
||||||
|
editorState.service.floatingShortcutServiceKey.currentState
|
||||||
|
?.unwrapOrNull<FlowyFloatingShortcutService>()
|
||||||
|
?.hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _clearFloatingShorts() {
|
void _updateSelection(Selection selection) {
|
||||||
final shortcutService = editorState
|
final nodes =
|
||||||
.service.floatingShortcutServiceKey.currentState
|
_selectedNodesInSelection(editorState.document.root, selection);
|
||||||
?.unwrapOrNull<FlowyFloatingShortcutService>();
|
|
||||||
shortcutService?.hide();
|
currentSelectedNodes = nodes;
|
||||||
|
|
||||||
|
var index = 0;
|
||||||
|
for (final node in nodes) {
|
||||||
|
final selectable = node.selectable;
|
||||||
|
if (selectable == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
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: abcd<ef
|
||||||
|
// text: ghijkl
|
||||||
|
// text: mn>opqr
|
||||||
|
if (index == 0) {
|
||||||
|
if (selection.isDownward) {
|
||||||
|
newSelection = selection.copyWith(end: selectable.end());
|
||||||
|
} else {
|
||||||
|
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(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final rects = selectable.getRectsInSelection(newSelection);
|
||||||
|
|
||||||
|
for (final rect in rects) {
|
||||||
|
final overlay = OverlayEntry(
|
||||||
|
builder: ((context) => SelectionWidget(
|
||||||
|
color: widget.selectionColor,
|
||||||
|
layerLink: node.layerLink,
|
||||||
|
rect: rect,
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
_selectionOverlays.add(overlay);
|
||||||
|
}
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
|
Overlay.of(context)?.insertAll(_selectionOverlays);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _updateCursor(Position position) {
|
||||||
|
final node = editorState.document.root.childAtPath(position.path);
|
||||||
|
|
||||||
|
assert(node != null);
|
||||||
|
if (node == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentSelectedNodes = [node];
|
||||||
|
|
||||||
|
final selectable = node.selectable;
|
||||||
|
final rect = selectable?.getCursorRectInPosition(position);
|
||||||
|
if (rect != null) {
|
||||||
|
final cursor = OverlayEntry(
|
||||||
|
builder: ((context) => CursorWidget(
|
||||||
|
key: _cursorKey,
|
||||||
|
rect: rect,
|
||||||
|
color: widget.cursorColor,
|
||||||
|
layerLink: node.layerLink,
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
_cursorOverlays.add(cursor);
|
||||||
|
Overlay.of(context)?.insertAll(_cursorOverlays);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Node> _selectedNodesInSelection(Node node, Selection selection) {
|
||||||
|
List<Node> result = [];
|
||||||
|
if (node.parent != null) {
|
||||||
|
if (node.inSelection(selection)) {
|
||||||
|
result.add(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (final child in node.children) {
|
||||||
|
result.addAll(_selectedNodesInSelection(child, selection));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import 'package:flowy_editor/service/floating_shortcut_service.dart';
|
import 'package:flowy_editor/service/shortcut_service.dart';
|
||||||
import 'package:flowy_editor/service/selection_service.dart';
|
import 'package:flowy_editor/service/selection_service.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
@ -127,7 +127,7 @@ void main() {
|
|||||||
final pos = Position(path: [0], offset: 0);
|
final pos = Position(path: [0], offset: 0);
|
||||||
final sel = Selection.collapsed(pos);
|
final sel = Selection.collapsed(pos);
|
||||||
expect(sel.start, sel.end);
|
expect(sel.start, sel.end);
|
||||||
expect(sel.isCollapsed(), true);
|
expect(sel.isCollapsed, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('test selection collapse', () {
|
test('test selection collapse', () {
|
||||||
|
Reference in New Issue
Block a user