feat: support floating selection and delete textnode

This commit is contained in:
Lucas.Xu 2022-07-25 14:14:40 +08:00
parent f58a6c9523
commit e1d990e4ae
13 changed files with 176 additions and 83 deletions

View File

@ -74,7 +74,7 @@
"type": "text",
"delta": [
{
"insert": "Click the '?' at the bottom right for help and support."
"insert": "1. Click the '?' at the bottom right for help and support."
}
],
"attributes": {}
@ -83,7 +83,7 @@
"type": "text",
"delta": [
{
"insert": "Click the '?' at the bottom right for help and support."
"insert": "2. Click the '?' at the bottom right for help and support."
}
],
"attributes": {}
@ -92,7 +92,7 @@
"type": "text",
"delta": [
{
"insert": "Click the '?' at the bottom right for help and support."
"insert": "3. Click the '?' at the bottom right for help and support."
}
],
"attributes": {}
@ -101,7 +101,7 @@
"type": "text",
"delta": [
{
"insert": "Click the '?' at the bottom right for help and support.Click the '?' at the bottom right for help and support.Click the '?' at the bottom right for help and support.Click the '?' at the bottom right for help and support.Click the '?' at the bottom right for help and support.Click the '?' at the bottom right for help and support.Click the '?' at the bottom right for help and support."
"insert": "4. Click the '?' at the bottom right for help and support.Click the '?' at the bottom right for help and support.Click the '?' at the bottom right for help and support.Click the '?' at the bottom right for help and support.Click the '?' at the bottom right for help and support.Click the '?' at the bottom right for help and support.Click the '?' at the bottom right for help and support."
}
],
"attributes": {}
@ -110,7 +110,7 @@
"type": "text",
"delta": [
{
"insert": "Click the '?' at the bottom right for help and support."
"insert": "5. Click the '?' at the bottom right for help and support."
}
],
"attributes": {}
@ -119,7 +119,7 @@
"type": "text",
"delta": [
{
"insert": "Click the '?' at the bottom right for help and support."
"insert": "6. Click the '?' at the bottom right for help and support."
}
],
"attributes": {}
@ -128,7 +128,7 @@
"type": "text",
"delta": [
{
"insert": "Click the '?' at the bottom right for help and support."
"insert": "7. Click the '?' at the bottom right for help and support."
}
],
"attributes": {}
@ -137,7 +137,7 @@
"type": "text",
"delta": [
{
"insert": "Click the '?' at the bottom right for help and support."
"insert": "8. Click the '?' at the bottom right for help and support."
}
],
"attributes": {}
@ -146,7 +146,7 @@
"type": "text",
"delta": [
{
"insert": "Click the '?' at the bottom right for help and support."
"insert": "9. Click the '?' at the bottom right for help and support."
}
],
"attributes": {}
@ -155,7 +155,7 @@
"type": "text",
"delta": [
{
"insert": "Click the '?' at the bottom right for help and support."
"insert": "10. Click the '?' at the bottom right for help and support."
}
],
"attributes": {}
@ -164,7 +164,7 @@
"type": "text",
"delta": [
{
"insert": "Click the '?' at the bottom right for help and support."
"insert": "11. Click the '?' at the bottom right for help and support."
}
],
"attributes": {}

View File

@ -96,9 +96,7 @@ class _MyHomePageState extends State<MyHomePage> {
);
return FlowyEditor(
editorState: _editorState,
keyEventHandler: [
deleteSingleImageNode,
],
keyEventHandler: const [],
);
}
},

View File

@ -1,17 +1,6 @@
import 'package:flowy_editor/flowy_editor.dart';
import 'package:flutter/material.dart';
FlowyKeyEventHandler deleteSingleImageNode = (editorState, event) {
final selectNodes = editorState.selectedNodes;
if (selectNodes.length != 1 || selectNodes.first.type != 'image') {
return KeyEventResult.ignored;
}
TransactionBuilder(editorState)
..deleteNode(selectNodes.first)
..commit();
return KeyEventResult.handled;
};
class ImageNodeBuilder extends NodeWidgetBuilder {
ImageNodeBuilder.create({
required super.node,
@ -67,6 +56,11 @@ class __ImageNodeWidgetState extends State<_ImageNodeWidget> with Selectable {
return null;
}
@override
Offset getOffsetByTextSelection(TextSelection textSelection) {
return Offset.zero;
}
@override
Widget build(BuildContext context) {
return _build(context);

View File

@ -93,6 +93,7 @@ class _SelectedTextNodeWidgetState extends State<_SelectedTextNodeWidget>
final selectionBaseOffset = _getTextPositionAtOffset(localStart).offset;
final textSelection = TextSelection.collapsed(offset: selectionBaseOffset);
_textSelection = textSelection;
print('text selection = $textSelection');
return _computeCursorRect(textSelection.baseOffset);
}
@ -101,6 +102,12 @@ class _SelectedTextNodeWidgetState extends State<_SelectedTextNodeWidget>
return _textSelection;
}
@override
Offset getOffsetByTextSelection(TextSelection textSelection) {
final offset = _computeCursorRect(textSelection.baseOffset).center;
return _renderParagraph.localToGlobal(offset);
}
@override
Widget build(BuildContext context) {
Widget richText;
@ -148,6 +155,7 @@ class _SelectedTextNodeWidgetState extends State<_SelectedTextNodeWidget>
final cursorOffset =
_renderParagraph.getOffsetForCaret(position, Rect.zero);
final cursorHeight = _renderParagraph.getFullHeightForCaret(position);
print('offset = $offset, cursorHeight = $cursorHeight');
if (cursorHeight != null) {
const cursorWidth = 2;
return Rect.fromLTWH(

View File

@ -19,6 +19,9 @@ class ApplyOptions {
});
}
// TODO
final selectionServiceKey = GlobalKey();
class EditorState {
final StateTree document;
final RenderPlugins renderPlugins;

View File

@ -0,0 +1,8 @@
extension FlowyObjectExtensions on Object {
T? unwrapOrNull<T>() {
if (this is T) {
return this as T;
}
return null;
}
}

View File

@ -11,5 +11,4 @@ export 'package:flowy_editor/operation/transaction.dart';
export 'package:flowy_editor/operation/transaction_builder.dart';
export 'package:flowy_editor/operation/operation.dart';
export 'package:flowy_editor/editor_state.dart';
export 'package:flowy_editor/service/flowy_editor_service.dart';
export 'package:flowy_editor/service/flowy_keyboard_service.dart';
export 'package:flowy_editor/service/editor_service.dart';

View File

@ -13,4 +13,7 @@ mixin Selectable<T extends StatefulWidget> on State<T> {
/// For [TextNode] only.
TextSelection? getTextSelection();
/// For [TextNode] only.
Offset getOffsetByTextSelection(TextSelection textSelection);
}

View File

@ -1,5 +1,7 @@
import 'package:flowy_editor/service/flowy_keyboard_service.dart';
import 'package:flowy_editor/service/flowy_selection_service.dart';
import 'package:flowy_editor/service/flowy_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/keyboard_service.dart';
import 'package:flowy_editor/service/selection_service.dart';
import '../editor_state.dart';
import 'package:flutter/material.dart';
@ -23,11 +25,13 @@ class _FlowyEditorState extends State<FlowyEditor> {
@override
Widget build(BuildContext context) {
return FlowySelectionService(
return FlowySelection(
key: selectionServiceKey,
editorState: editorState,
child: FlowyKeyboardWidget(
child: FlowyKeyboard(
handlers: [
flowyDeleteNodesHandler,
deleteSingleTextNodeHandler,
...widget.keyEventHandler,
],
editorState: editorState,

View File

@ -0,0 +1,21 @@
import 'package:flowy_editor/flowy_editor.dart';
import 'package:flowy_editor/service/keyboard_service.dart';
import 'package:flutter/material.dart';
FlowyKeyEventHandler flowyDeleteNodesHandler = (editorState, event) {
// Handle delete nodes.
final nodes = editorState.selectedNodes;
if (nodes.length <= 1) {
return KeyEventResult.ignored;
}
debugPrint('delete nodes = $nodes');
nodes
.fold<TransactionBuilder>(
TransactionBuilder(editorState),
(previousValue, node) => previousValue..deleteNode(node),
)
.commit();
return KeyEventResult.handled;
};

View File

@ -0,0 +1,73 @@
import 'package:flowy_editor/document/node.dart';
import 'package:flowy_editor/editor_state.dart';
import 'package:flowy_editor/operation/transaction_builder.dart';
import 'package:flowy_editor/render/selection/selectable.dart';
import 'package:flowy_editor/service/keyboard_service.dart';
import 'package:flowy_editor/extensions/object_extensions.dart';
import 'package:flowy_editor/service/selection_service.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
// TODO: need to be refactored, just a example code.
FlowyKeyEventHandler deleteSingleTextNodeHandler = (editorState, event) {
if (event.logicalKey != LogicalKeyboardKey.backspace) {
return KeyEventResult.ignored;
}
final selectionNodes = editorState.selectedNodes;
if (selectionNodes.length == 1 && selectionNodes.first is TextNode) {
final node = selectionNodes.first.unwrapOrNull<TextNode>();
final selectable = node?.key?.currentState?.unwrapOrNull<Selectable>();
if (selectable != null) {
final textSelection = selectable.getTextSelection();
if (textSelection != null) {
if (textSelection.isCollapsed) {
/// Three cases:
/// Delete the zero character,
/// 1. if there is still text node in front of it, then merge them.
/// 2. if not, just ignore
/// Delete the non-zero character,
/// 3. delete the single character.
if (textSelection.baseOffset == 0) {
if (node?.previous != null && node?.previous is TextNode) {
final previous = node!.previous! as TextNode;
final newTextSelection = TextSelection.collapsed(
offset: previous.toRawString().length);
final selectionService =
selectionServiceKey.currentState as FlowySelectionService;
final previousSelectable =
previous.key?.currentState?.unwrapOrNull<Selectable>();
final newOfset = previousSelectable
?.getOffsetByTextSelection(newTextSelection);
if (newOfset != null) {
selectionService.updateCursor(newOfset);
}
// merge
TransactionBuilder(editorState)
..deleteNode(node)
..insertText(
previous, previous.toRawString().length, node.toRawString())
..commit();
return KeyEventResult.handled;
} else {
return KeyEventResult.ignored;
}
} else {
TransactionBuilder(editorState)
..deleteText(node!, textSelection.baseOffset - 1, 1)
..commit();
final newTextSelection =
TextSelection.collapsed(offset: textSelection.baseOffset - 1);
final selectionService =
selectionServiceKey.currentState as FlowySelectionService;
final newOfset =
selectable.getOffsetByTextSelection(newTextSelection);
selectionService.updateCursor(newOfset);
return KeyEventResult.handled;
}
}
}
}
}
return KeyEventResult.ignored;
};

View File

@ -1,4 +1,3 @@
import 'package:flowy_editor/operation/transaction_builder.dart';
import 'package:flutter/services.dart';
import '../editor_state.dart';
@ -9,27 +8,9 @@ typedef FlowyKeyEventHandler = KeyEventResult Function(
RawKeyEvent event,
);
FlowyKeyEventHandler flowyDeleteNodesHandler = (editorState, event) {
// Handle delete nodes.
final nodes = editorState.selectedNodes;
if (nodes.length <= 1) {
return KeyEventResult.ignored;
}
debugPrint('delete nodes = $nodes');
nodes
.fold<TransactionBuilder>(
TransactionBuilder(editorState),
(previousValue, node) => previousValue..deleteNode(node),
)
.commit();
return KeyEventResult.handled;
};
/// Process keyboard events
class FlowyKeyboardWidget extends StatefulWidget {
const FlowyKeyboardWidget({
class FlowyKeyboard extends StatefulWidget {
const FlowyKeyboard({
Key? key,
required this.handlers,
required this.editorState,
@ -41,10 +22,10 @@ class FlowyKeyboardWidget extends StatefulWidget {
final List<FlowyKeyEventHandler> handlers;
@override
State<FlowyKeyboardWidget> createState() => _FlowyKeyboardWidgetState();
State<FlowyKeyboard> createState() => _FlowyKeyboardState();
}
class _FlowyKeyboardWidgetState extends State<FlowyKeyboardWidget> {
class _FlowyKeyboardState extends State<FlowyKeyboard> {
final FocusNode focusNode = FocusNode(debugLabel: 'flowy_keyboard_service');
@override

View File

@ -8,7 +8,7 @@ import '../document/node.dart';
import '../render/selection/selectable.dart';
/// 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.
/// Pan
Offset? panStartOffset;
@ -19,20 +19,20 @@ mixin _FlowySelectionService<T extends StatefulWidget> on State<T> {
void updateSelection(Offset start, Offset end);
void updateCursor(Offset offset);
void updateCursor(Offset start);
/// Returns selected node(s)
/// Returns empty list if no nodes are being selected.
List<Node> get selectedNodes;
List<Node> getSelectedNodes(Offset start, [Offset? end]);
/// Compute selected node triggered by [Tap]
Node? computeSelectedNodeByTap(
Node? computeSelectedNodeInOffset(
Node node,
Offset offset,
);
/// Compute selected nodes triggered by [Pan]
List<Node> computeSelectedNodesByPan(
List<Node> computeSelectedNodesInRange(
Node node,
Offset start,
Offset end,
@ -52,8 +52,8 @@ mixin _FlowySelectionService<T extends StatefulWidget> on State<T> {
);
}
class FlowySelectionService extends StatefulWidget {
const FlowySelectionService({
class FlowySelection extends StatefulWidget {
const FlowySelection({
Key? key,
required this.editorState,
required this.child,
@ -63,11 +63,11 @@ class FlowySelectionService extends StatefulWidget {
final Widget child;
@override
State<FlowySelectionService> createState() => _FlowySelectionServiceState();
State<FlowySelection> createState() => _FlowySelectionState();
}
class _FlowySelectionServiceState extends State<FlowySelectionService>
with _FlowySelectionService {
class _FlowySelectionState extends State<FlowySelection>
with FlowySelectionService {
final _cursorKey = GlobalKey(debugLabel: 'cursor');
final List<OverlayEntry> _selectionOverlays = [];
@ -106,7 +106,7 @@ class _FlowySelectionServiceState extends State<FlowySelectionService>
void updateSelection(Offset start, Offset end) {
_clearAllOverlayEntries();
final nodes = selectedNodes;
final nodes = getSelectedNodes(start, end);
editorState.selectedNodes = nodes;
if (nodes.isEmpty) {
return;
@ -133,10 +133,10 @@ class _FlowySelectionServiceState extends State<FlowySelectionService>
}
@override
void updateCursor(Offset offset) {
void updateCursor(Offset start) {
_clearAllOverlayEntries();
final nodes = selectedNodes;
final nodes = getSelectedNodes(start);
editorState.selectedNodes = nodes;
if (nodes.isEmpty) {
return;
@ -147,7 +147,7 @@ class _FlowySelectionServiceState extends State<FlowySelectionService>
return;
}
final selectable = selectedNode.key?.currentState as Selectable;
final rect = selectable.getCursorRect(offset);
final rect = selectable.getCursorRect(start);
final cursor = OverlayEntry(
builder: ((context) => FlowyCursorWidget(
key: _cursorKey,
@ -161,13 +161,18 @@ class _FlowySelectionServiceState extends State<FlowySelectionService>
}
@override
List<Node> get selectedNodes {
if (panStartOffset != null && panEndOffset != null) {
return computeSelectedNodesByPan(
editorState.document.root, panStartOffset!, panEndOffset!);
} else if (tapOffset != null) {
final reuslt =
computeSelectedNodeByTap(editorState.document.root, tapOffset!);
List<Node> getSelectedNodes(Offset start, [Offset? end]) {
if (end != null) {
return computeSelectedNodesInRange(
editorState.document.root,
start,
end,
);
} else {
final reuslt = computeSelectedNodeInOffset(
editorState.document.root,
start,
);
if (reuslt != null) {
return [reuslt];
}
@ -176,13 +181,9 @@ class _FlowySelectionServiceState extends State<FlowySelectionService>
}
@override
Node? computeSelectedNodeByTap(Node node, Offset offset) {
assert(this.tapOffset != null);
final tapOffset = this.tapOffset;
if (tapOffset != null) {}
Node? computeSelectedNodeInOffset(Node node, Offset offset) {
for (final child in node.children) {
final result = computeSelectedNodeByTap(child, offset);
final result = computeSelectedNodeInOffset(child, offset);
if (result != null) {
return result;
}
@ -198,7 +199,7 @@ class _FlowySelectionServiceState extends State<FlowySelectionService>
}
@override
List<Node> computeSelectedNodesByPan(Node node, Offset start, Offset end) {
List<Node> computeSelectedNodesInRange(Node node, Offset start, Offset end) {
List<Node> result = [];
if (node.parent != null && node.key != null) {
if (isNodeInSelection(node, start, end)) {
@ -206,7 +207,7 @@ class _FlowySelectionServiceState extends State<FlowySelectionService>
}
}
for (final child in node.children) {
result.addAll(computeSelectedNodesByPan(child, start, end));
result.addAll(computeSelectedNodesInRange(child, start, end));
}
// TODO: sort the result
return result;