mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: add keyboard example
This commit is contained in:
parent
a831ddc589
commit
c643c02887
@ -1,5 +1,4 @@
|
|||||||
import 'package:flowy_editor/flowy_editor.dart';
|
import 'package:flowy_editor/flowy_editor.dart';
|
||||||
import 'package:flutter/gestures.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class EditorNodeWidgetBuilder extends NodeWidgetBuilder {
|
class EditorNodeWidgetBuilder extends NodeWidgetBuilder {
|
||||||
|
@ -57,6 +57,11 @@ class __ImageNodeWidgetState extends State<_ImageNodeWidget>
|
|||||||
return cursorOffset & size;
|
return cursorOffset & size;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
TextSelection? getTextSelection() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
KeyEventResult onKeyDown(RawKeyEvent event) {
|
KeyEventResult onKeyDown(RawKeyEvent event) {
|
||||||
if (event.logicalKey == LogicalKeyboardKey.backspace) {
|
if (event.logicalKey == LogicalKeyboardKey.backspace) {
|
||||||
|
@ -94,6 +94,11 @@ class _SelectedTextNodeWidgetState extends State<_SelectedTextNodeWidget>
|
|||||||
return _computeCursorRect(textSelection.baseOffset);
|
return _computeCursorRect(textSelection.baseOffset);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
TextSelection? getTextSelection() {
|
||||||
|
return _textSelection;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
KeyEventResult onKeyDown(RawKeyEvent event) {
|
KeyEventResult onKeyDown(RawKeyEvent event) {
|
||||||
if (event.logicalKey == LogicalKeyboardKey.backspace) {
|
if (event.logicalKey == LogicalKeyboardKey.backspace) {
|
||||||
@ -111,9 +116,9 @@ class _SelectedTextNodeWidgetState extends State<_SelectedTextNodeWidget>
|
|||||||
TransactionBuilder(editorState)
|
TransactionBuilder(editorState)
|
||||||
..deleteText(node, textSelection.start - 1, 1)
|
..deleteText(node, textSelection.start - 1, 1)
|
||||||
..commit();
|
..commit();
|
||||||
final rect = _computeCursorRect(textSelection.baseOffset - 1);
|
// final rect = _computeCursorRect(textSelection.baseOffset - 1);
|
||||||
editorState.tapOffset = rect.center;
|
// editorState.tapOffset = rect.center;
|
||||||
editorState.updateCursor();
|
// editorState.updateCursor();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
TransactionBuilder(editorState)
|
TransactionBuilder(editorState)
|
||||||
|
@ -15,10 +15,7 @@ import './render/render_plugins.dart';
|
|||||||
class EditorState {
|
class EditorState {
|
||||||
final StateTree document;
|
final StateTree document;
|
||||||
final RenderPlugins renderPlugins;
|
final RenderPlugins renderPlugins;
|
||||||
|
List<Node> selectedNodes = [];
|
||||||
Offset? tapOffset;
|
|
||||||
Offset? panStartOffset;
|
|
||||||
Offset? panEndOffset;
|
|
||||||
|
|
||||||
Selection? cursorSelection;
|
Selection? cursorSelection;
|
||||||
|
|
||||||
@ -59,148 +56,4 @@ class EditorState {
|
|||||||
document.textEdit(op.path, op.delta);
|
document.textEdit(op.path, op.delta);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
List<OverlayEntry> selectionOverlays = [];
|
|
||||||
|
|
||||||
void updateCursor() {
|
|
||||||
selectionOverlays
|
|
||||||
..forEach((element) => element.remove())
|
|
||||||
..clear();
|
|
||||||
|
|
||||||
if (tapOffset == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: upward and backward
|
|
||||||
final selectedNode = _calculateSelectedNode(document.root, tapOffset!);
|
|
||||||
if (selectedNode.isEmpty) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final key = selectedNode.first.key;
|
|
||||||
if (key != null && key.currentState is Selectable) {
|
|
||||||
final selectable = key.currentState as Selectable;
|
|
||||||
final rect = selectable.getCursorRect(tapOffset!);
|
|
||||||
final overlay = OverlayEntry(builder: ((context) {
|
|
||||||
return Positioned.fromRect(
|
|
||||||
rect: rect,
|
|
||||||
child: Container(
|
|
||||||
color: Colors.red,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}));
|
|
||||||
selectionOverlays.add(overlay);
|
|
||||||
Overlay.of(selectable.context)?.insert(overlay);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void updateSelection() {
|
|
||||||
selectionOverlays
|
|
||||||
..forEach((element) => element.remove())
|
|
||||||
..clear();
|
|
||||||
|
|
||||||
final selectedNodes = this.selectedNodes;
|
|
||||||
if (selectedNodes.isEmpty ||
|
|
||||||
panStartOffset == null ||
|
|
||||||
panEndOffset == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (final node in selectedNodes) {
|
|
||||||
final key = node.key;
|
|
||||||
if (key != null && key.currentState is Selectable) {
|
|
||||||
final selectable = key.currentState as Selectable;
|
|
||||||
final overlayRects = selectable.getSelectionRectsInSelection(
|
|
||||||
panStartOffset!, panEndOffset!);
|
|
||||||
for (final rect in overlayRects) {
|
|
||||||
// TODO: refactor overlay implement.
|
|
||||||
final overlay = OverlayEntry(builder: ((context) {
|
|
||||||
return Positioned.fromRect(
|
|
||||||
rect: rect,
|
|
||||||
child: Container(
|
|
||||||
color: Colors.yellow.withAlpha(100),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}));
|
|
||||||
selectionOverlays.add(overlay);
|
|
||||||
Overlay.of(selectable.context)?.insert(overlay);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
List<Node> get selectedNodes {
|
|
||||||
if (panStartOffset != null && panEndOffset != null) {
|
|
||||||
return _calculateSelectedNodes(
|
|
||||||
document.root, panStartOffset!, panEndOffset!);
|
|
||||||
}
|
|
||||||
if (tapOffset != null) {
|
|
||||||
return _calculateSelectedNode(document.root, tapOffset!);
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
List<Node> _calculateSelectedNode(Node node, Offset offset) {
|
|
||||||
List<Node> result = [];
|
|
||||||
|
|
||||||
/// Skip the node without parent because it is the topmost node.
|
|
||||||
/// Skip the node without key because it cannot get the [RenderObject].
|
|
||||||
if (node.parent != null && node.key != null) {
|
|
||||||
if (_isNodeInOffset(node, offset)) {
|
|
||||||
result.add(node);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
///
|
|
||||||
for (final child in node.children) {
|
|
||||||
result.addAll(_calculateSelectedNode(child, offset));
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool _isNodeInOffset(Node node, Offset offset) {
|
|
||||||
assert(node.key != null);
|
|
||||||
final renderBox =
|
|
||||||
node.key?.currentContext?.findRenderObject() as RenderBox?;
|
|
||||||
if (renderBox == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
final boxOffset = renderBox.localToGlobal(Offset.zero);
|
|
||||||
final boxRect = boxOffset & renderBox.size;
|
|
||||||
return boxRect.contains(offset);
|
|
||||||
}
|
|
||||||
|
|
||||||
List<Node> _calculateSelectedNodes(Node node, Offset start, Offset end) {
|
|
||||||
List<Node> result = [];
|
|
||||||
|
|
||||||
/// Skip the node without parent because it is the topmost node.
|
|
||||||
/// Skip the node without key because it cannot get the [RenderObject].
|
|
||||||
if (node.parent != null && node.key != null) {
|
|
||||||
if (_isNodeInRange(node, start, end)) {
|
|
||||||
result.add(node);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
///
|
|
||||||
for (final child in node.children) {
|
|
||||||
result.addAll(_calculateSelectedNodes(child, start, end));
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool _isNodeInRange(Node node, Offset start, Offset end) {
|
|
||||||
assert(node.key != null);
|
|
||||||
final renderBox =
|
|
||||||
node.key?.currentContext?.findRenderObject() as RenderBox?;
|
|
||||||
|
|
||||||
/// Return false directly if the [RenderBox] cannot found.
|
|
||||||
if (renderBox == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
final rect = Rect.fromPoints(start, end);
|
|
||||||
final boxOffset = renderBox.localToGlobal(Offset.zero);
|
|
||||||
return rect.overlaps(boxOffset & renderBox.size);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -24,7 +24,11 @@ class _FlowyEditorState extends State<FlowyEditor> {
|
|||||||
return FlowySelectionWidget(
|
return FlowySelectionWidget(
|
||||||
editorState: editorState,
|
editorState: editorState,
|
||||||
child: FlowyKeyboardWidget(
|
child: FlowyKeyboardWidget(
|
||||||
handlers: const [],
|
handlers: [
|
||||||
|
FlowyKeyboradBackSpaceHandler(
|
||||||
|
editorState: editorState,
|
||||||
|
)
|
||||||
|
],
|
||||||
editorState: editorState,
|
editorState: editorState,
|
||||||
child: editorState.build(context),
|
child: editorState.build(context),
|
||||||
),
|
),
|
||||||
|
@ -1,3 +1,7 @@
|
|||||||
|
import 'package:flowy_editor/document/node.dart';
|
||||||
|
import 'package:flowy_editor/operation/transaction.dart';
|
||||||
|
import 'package:flowy_editor/operation/transaction_builder.dart';
|
||||||
|
import 'package:flowy_editor/render/selectable.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
import 'editor_state.dart';
|
import 'editor_state.dart';
|
||||||
@ -5,14 +9,44 @@ import 'package:flutter/material.dart';
|
|||||||
|
|
||||||
abstract class FlowyKeyboardHandler {
|
abstract class FlowyKeyboardHandler {
|
||||||
final EditorState editorState;
|
final EditorState editorState;
|
||||||
final RawKeyEvent rawKeyEvent;
|
|
||||||
|
|
||||||
FlowyKeyboardHandler({
|
FlowyKeyboardHandler({
|
||||||
required this.editorState,
|
required this.editorState,
|
||||||
required this.rawKeyEvent,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
KeyEventResult onKeyDown();
|
KeyEventResult onKeyDown(RawKeyEvent event);
|
||||||
|
}
|
||||||
|
|
||||||
|
class FlowyKeyboradBackSpaceHandler extends FlowyKeyboardHandler {
|
||||||
|
FlowyKeyboradBackSpaceHandler({
|
||||||
|
required super.editorState,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
KeyEventResult onKeyDown(RawKeyEvent event) {
|
||||||
|
final selectedNodes = editorState.selectedNodes;
|
||||||
|
if (selectedNodes.isNotEmpty) {
|
||||||
|
// handle delete text
|
||||||
|
// TODO: type: cursor or selection
|
||||||
|
if (selectedNodes.length == 1) {
|
||||||
|
final node = selectedNodes.first;
|
||||||
|
if (node is TextNode) {
|
||||||
|
final selectable = node.key?.currentState as Selectable?;
|
||||||
|
final textSelection = selectable?.getTextSelection();
|
||||||
|
if (textSelection != null) {
|
||||||
|
if (textSelection.isCollapsed) {
|
||||||
|
TransactionBuilder(editorState)
|
||||||
|
..deleteText(node, textSelection.start - 1, 1)
|
||||||
|
..commit();
|
||||||
|
// TODO: update selection??
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return KeyEventResult.handled;
|
||||||
|
}
|
||||||
|
return KeyEventResult.ignored;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Process keyboard events
|
/// Process keyboard events
|
||||||
@ -46,6 +80,8 @@ class _FlowyKeyboardWidgetState extends State<FlowyKeyboardWidget> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
KeyEventResult _onKey(FocusNode node, RawKeyEvent event) {
|
KeyEventResult _onKey(FocusNode node, RawKeyEvent event) {
|
||||||
|
debugPrint('on keyboard event $event');
|
||||||
|
|
||||||
if (event is! RawKeyDownEvent) {
|
if (event is! RawKeyDownEvent) {
|
||||||
return KeyEventResult.ignored;
|
return KeyEventResult.ignored;
|
||||||
}
|
}
|
||||||
@ -53,7 +89,7 @@ class _FlowyKeyboardWidgetState extends State<FlowyKeyboardWidget> {
|
|||||||
for (final handler in widget.handlers) {
|
for (final handler in widget.handlers) {
|
||||||
debugPrint('handle keyboard event $event by $handler');
|
debugPrint('handle keyboard event $event by $handler');
|
||||||
|
|
||||||
KeyEventResult result = handler.onKeyDown();
|
KeyEventResult result = handler.onKeyDown(event);
|
||||||
|
|
||||||
switch (result) {
|
switch (result) {
|
||||||
case KeyEventResult.handled:
|
case KeyEventResult.handled:
|
||||||
|
@ -102,6 +102,7 @@ class _FlowySelectionWidgetState extends State<FlowySelectionWidget>
|
|||||||
_clearOverlay();
|
_clearOverlay();
|
||||||
|
|
||||||
final nodes = selectedNodes;
|
final nodes = selectedNodes;
|
||||||
|
editorState.selectedNodes = nodes;
|
||||||
if (nodes.isEmpty || panStartOffset == null || panEndOffset == null) {
|
if (nodes.isEmpty || panStartOffset == null || panEndOffset == null) {
|
||||||
assert(panStartOffset == null);
|
assert(panStartOffset == null);
|
||||||
assert(panEndOffset == null);
|
assert(panEndOffset == null);
|
||||||
@ -139,6 +140,7 @@ class _FlowySelectionWidgetState extends State<FlowySelectionWidget>
|
|||||||
}
|
}
|
||||||
|
|
||||||
final nodes = selectedNodes;
|
final nodes = selectedNodes;
|
||||||
|
editorState.selectedNodes = nodes;
|
||||||
if (nodes.isEmpty) {
|
if (nodes.isEmpty) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -26,20 +26,20 @@ class Keyboard extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
KeyEventResult _onKey(FocusNode node, RawKeyEvent event) {
|
KeyEventResult _onKey(FocusNode node, RawKeyEvent event) {
|
||||||
if (event is! RawKeyDownEvent) {
|
// if (event is! RawKeyDownEvent) {
|
||||||
return KeyEventResult.ignored;
|
// return KeyEventResult.ignored;
|
||||||
}
|
// }
|
||||||
List<KeyEventResult> result = [];
|
// List<KeyEventResult> result = [];
|
||||||
for (final node in editorState.selectedNodes) {
|
// for (final node in editorState.selectedNodes) {
|
||||||
if (node.key != null &&
|
// if (node.key != null &&
|
||||||
node.key?.currentState is KeyboardEventsRespondable) {
|
// node.key?.currentState is KeyboardEventsRespondable) {
|
||||||
final respondable = node.key!.currentState as KeyboardEventsRespondable;
|
// final respondable = node.key!.currentState as KeyboardEventsRespondable;
|
||||||
result.add(respondable.onKeyDown(event));
|
// result.add(respondable.onKeyDown(event));
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
if (result.contains(KeyEventResult.handled)) {
|
// if (result.contains(KeyEventResult.handled)) {
|
||||||
return KeyEventResult.handled;
|
// return KeyEventResult.handled;
|
||||||
}
|
// }
|
||||||
return KeyEventResult.ignored;
|
return KeyEventResult.ignored;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,8 +6,11 @@ mixin Selectable<T extends StatefulWidget> on State<T> {
|
|||||||
/// [start] and [end] are global offsets.
|
/// [start] and [end] are global offsets.
|
||||||
List<Rect> getSelectionRectsInSelection(Offset start, Offset end);
|
List<Rect> getSelectionRectsInSelection(Offset start, Offset end);
|
||||||
|
|
||||||
/// Returns a [Rect] for cursor
|
/// Returns a [Rect] for cursor.
|
||||||
Rect getCursorRect(Offset start);
|
Rect getCursorRect(Offset start);
|
||||||
|
|
||||||
|
/// For [TextNode] only.
|
||||||
|
TextSelection? getTextSelection();
|
||||||
}
|
}
|
||||||
|
|
||||||
mixin KeyboardEventsRespondable<T extends StatefulWidget> on State<T> {
|
mixin KeyboardEventsRespondable<T extends StatefulWidget> on State<T> {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user