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:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class EditorNodeWidgetBuilder extends NodeWidgetBuilder {
|
||||
|
@ -57,6 +57,11 @@ class __ImageNodeWidgetState extends State<_ImageNodeWidget>
|
||||
return cursorOffset & size;
|
||||
}
|
||||
|
||||
@override
|
||||
TextSelection? getTextSelection() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
KeyEventResult onKeyDown(RawKeyEvent event) {
|
||||
if (event.logicalKey == LogicalKeyboardKey.backspace) {
|
||||
|
@ -94,6 +94,11 @@ class _SelectedTextNodeWidgetState extends State<_SelectedTextNodeWidget>
|
||||
return _computeCursorRect(textSelection.baseOffset);
|
||||
}
|
||||
|
||||
@override
|
||||
TextSelection? getTextSelection() {
|
||||
return _textSelection;
|
||||
}
|
||||
|
||||
@override
|
||||
KeyEventResult onKeyDown(RawKeyEvent event) {
|
||||
if (event.logicalKey == LogicalKeyboardKey.backspace) {
|
||||
@ -111,9 +116,9 @@ class _SelectedTextNodeWidgetState extends State<_SelectedTextNodeWidget>
|
||||
TransactionBuilder(editorState)
|
||||
..deleteText(node, textSelection.start - 1, 1)
|
||||
..commit();
|
||||
final rect = _computeCursorRect(textSelection.baseOffset - 1);
|
||||
editorState.tapOffset = rect.center;
|
||||
editorState.updateCursor();
|
||||
// final rect = _computeCursorRect(textSelection.baseOffset - 1);
|
||||
// editorState.tapOffset = rect.center;
|
||||
// editorState.updateCursor();
|
||||
}
|
||||
} else {
|
||||
TransactionBuilder(editorState)
|
||||
|
@ -15,10 +15,7 @@ import './render/render_plugins.dart';
|
||||
class EditorState {
|
||||
final StateTree document;
|
||||
final RenderPlugins renderPlugins;
|
||||
|
||||
Offset? tapOffset;
|
||||
Offset? panStartOffset;
|
||||
Offset? panEndOffset;
|
||||
List<Node> selectedNodes = [];
|
||||
|
||||
Selection? cursorSelection;
|
||||
|
||||
@ -59,148 +56,4 @@ class EditorState {
|
||||
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(
|
||||
editorState: editorState,
|
||||
child: FlowyKeyboardWidget(
|
||||
handlers: const [],
|
||||
handlers: [
|
||||
FlowyKeyboradBackSpaceHandler(
|
||||
editorState: editorState,
|
||||
)
|
||||
],
|
||||
editorState: editorState,
|
||||
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 'editor_state.dart';
|
||||
@ -5,14 +9,44 @@ import 'package:flutter/material.dart';
|
||||
|
||||
abstract class FlowyKeyboardHandler {
|
||||
final EditorState editorState;
|
||||
final RawKeyEvent rawKeyEvent;
|
||||
|
||||
FlowyKeyboardHandler({
|
||||
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
|
||||
@ -46,6 +80,8 @@ class _FlowyKeyboardWidgetState extends State<FlowyKeyboardWidget> {
|
||||
}
|
||||
|
||||
KeyEventResult _onKey(FocusNode node, RawKeyEvent event) {
|
||||
debugPrint('on keyboard event $event');
|
||||
|
||||
if (event is! RawKeyDownEvent) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
@ -53,7 +89,7 @@ class _FlowyKeyboardWidgetState extends State<FlowyKeyboardWidget> {
|
||||
for (final handler in widget.handlers) {
|
||||
debugPrint('handle keyboard event $event by $handler');
|
||||
|
||||
KeyEventResult result = handler.onKeyDown();
|
||||
KeyEventResult result = handler.onKeyDown(event);
|
||||
|
||||
switch (result) {
|
||||
case KeyEventResult.handled:
|
||||
|
@ -102,6 +102,7 @@ class _FlowySelectionWidgetState extends State<FlowySelectionWidget>
|
||||
_clearOverlay();
|
||||
|
||||
final nodes = selectedNodes;
|
||||
editorState.selectedNodes = nodes;
|
||||
if (nodes.isEmpty || panStartOffset == null || panEndOffset == null) {
|
||||
assert(panStartOffset == null);
|
||||
assert(panEndOffset == null);
|
||||
@ -139,6 +140,7 @@ class _FlowySelectionWidgetState extends State<FlowySelectionWidget>
|
||||
}
|
||||
|
||||
final nodes = selectedNodes;
|
||||
editorState.selectedNodes = nodes;
|
||||
if (nodes.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
@ -26,20 +26,20 @@ class Keyboard extends StatelessWidget {
|
||||
}
|
||||
|
||||
KeyEventResult _onKey(FocusNode node, RawKeyEvent event) {
|
||||
if (event is! RawKeyDownEvent) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
List<KeyEventResult> result = [];
|
||||
for (final node in editorState.selectedNodes) {
|
||||
if (node.key != null &&
|
||||
node.key?.currentState is KeyboardEventsRespondable) {
|
||||
final respondable = node.key!.currentState as KeyboardEventsRespondable;
|
||||
result.add(respondable.onKeyDown(event));
|
||||
}
|
||||
}
|
||||
if (result.contains(KeyEventResult.handled)) {
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
// if (event is! RawKeyDownEvent) {
|
||||
// return KeyEventResult.ignored;
|
||||
// }
|
||||
// List<KeyEventResult> result = [];
|
||||
// for (final node in editorState.selectedNodes) {
|
||||
// if (node.key != null &&
|
||||
// node.key?.currentState is KeyboardEventsRespondable) {
|
||||
// final respondable = node.key!.currentState as KeyboardEventsRespondable;
|
||||
// result.add(respondable.onKeyDown(event));
|
||||
// }
|
||||
// }
|
||||
// if (result.contains(KeyEventResult.handled)) {
|
||||
// return KeyEventResult.handled;
|
||||
// }
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
}
|
||||
|
@ -6,8 +6,11 @@ mixin Selectable<T extends StatefulWidget> on State<T> {
|
||||
/// [start] and [end] are global offsets.
|
||||
List<Rect> getSelectionRectsInSelection(Offset start, Offset end);
|
||||
|
||||
/// Returns a [Rect] for cursor
|
||||
/// Returns a [Rect] for cursor.
|
||||
Rect getCursorRect(Offset start);
|
||||
|
||||
/// For [TextNode] only.
|
||||
TextSelection? getTextSelection();
|
||||
}
|
||||
|
||||
mixin KeyboardEventsRespondable<T extends StatefulWidget> on State<T> {
|
||||
|
Loading…
Reference in New Issue
Block a user