mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: implement autoscrolling on edge touch
This commit is contained in:
parent
90fa1312f2
commit
e5787090d2
@ -241,6 +241,62 @@
|
|||||||
"subtype": "number-list",
|
"subtype": "number-list",
|
||||||
"number": 3
|
"number": 3
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"insert": "At AppFlowy, we embody what we value deep in our hearts, taking inspiration from other great companies while forging our own path. AppFlowy’s five core values are Mission Driven, Aim High & Iterate, Transparency, Collaboration, and Honesty. Together, they spell MATCH. We will continue to iterate and refine these values as we grow."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"insert": "At AppFlowy, we embody what we value deep in our hearts, taking inspiration from other great companies while forging our own path. AppFlowy’s five core values are Mission Driven, Aim High & Iterate, Transparency, Collaboration, and Honesty. Together, they spell MATCH. We will continue to iterate and refine these values as we grow."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"insert": "At AppFlowy, we embody what we value deep in our hearts, taking inspiration from other great companies while forging our own path. AppFlowy’s five core values are Mission Driven, Aim High & Iterate, Transparency, Collaboration, and Honesty. Together, they spell MATCH. We will continue to iterate and refine these values as we grow."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"insert": "At AppFlowy, we embody what we value deep in our hearts, taking inspiration from other great companies while forging our own path. AppFlowy’s five core values are Mission Driven, Aim High & Iterate, Transparency, Collaboration, and Honesty. Together, they spell MATCH. We will continue to iterate and refine these values as we grow."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"insert": "At AppFlowy, we embody what we value deep in our hearts, taking inspiration from other great companies while forging our own path. AppFlowy’s five core values are Mission Driven, Aim High & Iterate, Transparency, Collaboration, and Honesty. Together, they spell MATCH. We will continue to iterate and refine these values as we grow."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"insert": "At AppFlowy, we embody what we value deep in our hearts, taking inspiration from other great companies while forging our own path. AppFlowy’s five core values are Mission Driven, Aim High & Iterate, Transparency, Collaboration, and Honesty. Together, they spell MATCH. We will continue to iterate and refine these values as we grow."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"delta": [
|
||||||
|
{
|
||||||
|
"insert": "At AppFlowy, we embody what we value deep in our hearts, taking inspiration from other great companies while forging our own path. AppFlowy’s five core values are Mission Driven, Aim High & Iterate, Transparency, Collaboration, and Honesty. Together, they spell MATCH. We will continue to iterate and refine these values as we grow."
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:flowy_editor/document/node.dart';
|
import 'package:flowy_editor/document/node.dart';
|
||||||
import 'package:flowy_editor/editor_state.dart';
|
import 'package:flowy_editor/editor_state.dart';
|
||||||
import 'package:flowy_editor/service/render_plugin_service.dart';
|
import 'package:flowy_editor/service/render_plugin_service.dart';
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
class EditorEntryWidgetBuilder extends NodeWidgetBuilder<Node> {
|
class EditorEntryWidgetBuilder extends NodeWidgetBuilder<Node> {
|
||||||
@override
|
@override
|
||||||
@ -31,28 +32,26 @@ class EditorNodeWidget extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return SingleChildScrollView(
|
return Column(
|
||||||
child: Column(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
children: node.children
|
||||||
children: node.children
|
.map(
|
||||||
.map(
|
(child) =>
|
||||||
(child) =>
|
editorState.service.renderPluginService.buildPluginWidget(
|
||||||
editorState.service.renderPluginService.buildPluginWidget(
|
child is TextNode
|
||||||
child is TextNode
|
? NodeWidgetContext<TextNode>(
|
||||||
? NodeWidgetContext<TextNode>(
|
context: context,
|
||||||
context: context,
|
node: child,
|
||||||
node: child,
|
editorState: editorState,
|
||||||
editorState: editorState,
|
)
|
||||||
)
|
: NodeWidgetContext<Node>(
|
||||||
: NodeWidgetContext<Node>(
|
context: context,
|
||||||
context: context,
|
node: child,
|
||||||
node: child,
|
editorState: editorState,
|
||||||
editorState: editorState,
|
),
|
||||||
),
|
),
|
||||||
),
|
)
|
||||||
)
|
.toList(),
|
||||||
.toList(),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,16 +1,16 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/rendering.dart';
|
||||||
|
|
||||||
import 'package:flowy_editor/document/node.dart';
|
import 'package:flowy_editor/document/node.dart';
|
||||||
|
import 'package:flowy_editor/document/path.dart';
|
||||||
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:flowy_editor/document/text_delta.dart';
|
||||||
import 'package:flowy_editor/editor_state.dart';
|
import 'package:flowy_editor/editor_state.dart';
|
||||||
import 'package:flowy_editor/document/path.dart';
|
|
||||||
import 'package:flowy_editor/render/rich_text/rich_text_style.dart';
|
import 'package:flowy_editor/render/rich_text/rich_text_style.dart';
|
||||||
import 'package:flowy_editor/render/selection/selectable.dart';
|
import 'package:flowy_editor/render/selection/selectable.dart';
|
||||||
import 'package:flowy_editor/service/render_plugin_service.dart';
|
import 'package:flowy_editor/service/render_plugin_service.dart';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter/rendering.dart';
|
|
||||||
|
|
||||||
class RichTextNodeWidgetBuilder extends NodeWidgetBuilder<TextNode> {
|
class RichTextNodeWidgetBuilder extends NodeWidgetBuilder<TextNode> {
|
||||||
@override
|
@override
|
||||||
Widget build(NodeWidgetContext<TextNode> context) {
|
Widget build(NodeWidgetContext<TextNode> context) {
|
||||||
@ -173,11 +173,12 @@ class _FlowyRichTextState extends State<FlowyRichText> with Selectable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
TextSpan get _textSpan => TextSpan(
|
TextSpan get _textSpan => TextSpan(
|
||||||
children: widget.textNode.delta.operations
|
children: widget.textNode.delta.operations
|
||||||
.whereType<TextInsert>()
|
.whereType<TextInsert>()
|
||||||
.map((insert) => RichTextStyle(
|
.map((insert) => RichTextStyle(
|
||||||
attributes: insert.attributes ?? {},
|
attributes: insert.attributes ?? {},
|
||||||
text: insert.content,
|
text: insert.content,
|
||||||
).toTextSpan())
|
).toTextSpan())
|
||||||
.toList(growable: false));
|
.toList(growable: false),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import 'package:flowy_editor/service/internal_key_event_handlers/delete_text_handler.dart';
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:flowy_editor/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:flowy_editor/editor_state.dart';
|
import 'package:flowy_editor/editor_state.dart';
|
||||||
@ -13,10 +12,13 @@ import 'package:flowy_editor/render/rich_text/quoted_text.dart';
|
|||||||
import 'package:flowy_editor/service/input_service.dart';
|
import 'package:flowy_editor/service/input_service.dart';
|
||||||
import 'package:flowy_editor/service/internal_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/internal_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/internal_key_event_handlers/delete_text_handler.dart';
|
||||||
import 'package:flowy_editor/service/internal_key_event_handlers/enter_in_edge_of_text_node_handler.dart';
|
import 'package:flowy_editor/service/internal_key_event_handlers/enter_in_edge_of_text_node_handler.dart';
|
||||||
import 'package:flowy_editor/service/internal_key_event_handlers/slash_handler.dart';
|
import 'package:flowy_editor/service/internal_key_event_handlers/slash_handler.dart';
|
||||||
|
import 'package:flowy_editor/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart';
|
||||||
import 'package:flowy_editor/service/keyboard_service.dart';
|
import 'package:flowy_editor/service/keyboard_service.dart';
|
||||||
import 'package:flowy_editor/service/render_plugin_service.dart';
|
import 'package:flowy_editor/service/render_plugin_service.dart';
|
||||||
|
import 'package:flowy_editor/service/scroll_service.dart';
|
||||||
import 'package:flowy_editor/service/selection_service.dart';
|
import 'package:flowy_editor/service/selection_service.dart';
|
||||||
import 'package:flowy_editor/service/toolbar_service.dart';
|
import 'package:flowy_editor/service/toolbar_service.dart';
|
||||||
|
|
||||||
@ -60,15 +62,25 @@ class FlowyEditor extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _FlowyEditorState extends State<FlowyEditor> {
|
class _FlowyEditorState extends State<FlowyEditor> {
|
||||||
|
late ScrollController _scrollController;
|
||||||
|
|
||||||
EditorState get editorState => widget.editorState;
|
EditorState get editorState => widget.editorState;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
|
_scrollController = ScrollController()..addListener(_scrollCallback);
|
||||||
editorState.service.renderPluginService = _createRenderPlugin();
|
editorState.service.renderPluginService = _createRenderPlugin();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_scrollController.dispose();
|
||||||
|
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void didUpdateWidget(covariant FlowyEditor oldWidget) {
|
void didUpdateWidget(covariant FlowyEditor oldWidget) {
|
||||||
super.didUpdateWidget(oldWidget);
|
super.didUpdateWidget(oldWidget);
|
||||||
@ -80,33 +92,36 @@ class _FlowyEditorState extends State<FlowyEditor> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return FlowySelection(
|
return FlowyScroll(
|
||||||
key: editorState.service.selectionServiceKey,
|
key: editorState.service.scrollServiceKey,
|
||||||
editorState: editorState,
|
child: FlowySelection(
|
||||||
child: FlowyInput(
|
key: editorState.service.selectionServiceKey,
|
||||||
key: editorState.service.inputServiceKey,
|
|
||||||
editorState: editorState,
|
|
||||||
child: FlowyKeyboard(
|
|
||||||
key: editorState.service.keyboardServiceKey,
|
|
||||||
handlers: [
|
|
||||||
...defaultKeyEventHandler,
|
|
||||||
...widget.keyEventHandlers,
|
|
||||||
],
|
|
||||||
editorState: editorState,
|
editorState: editorState,
|
||||||
child: FlowyToolbar(
|
child: FlowyInput(
|
||||||
key: editorState.service.toolbarServiceKey,
|
key: editorState.service.inputServiceKey,
|
||||||
editorState: editorState,
|
editorState: editorState,
|
||||||
child: editorState.service.renderPluginService.buildPluginWidget(
|
child: FlowyKeyboard(
|
||||||
NodeWidgetContext(
|
key: editorState.service.keyboardServiceKey,
|
||||||
context: context,
|
handlers: [
|
||||||
node: editorState.document.root,
|
...defaultKeyEventHandler,
|
||||||
|
...widget.keyEventHandlers,
|
||||||
|
],
|
||||||
|
editorState: editorState,
|
||||||
|
child: FlowyToolbar(
|
||||||
|
key: editorState.service.toolbarServiceKey,
|
||||||
editorState: editorState,
|
editorState: editorState,
|
||||||
|
child:
|
||||||
|
editorState.service.renderPluginService.buildPluginWidget(
|
||||||
|
NodeWidgetContext(
|
||||||
|
context: context,
|
||||||
|
node: editorState.document.root,
|
||||||
|
editorState: editorState,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
));
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
FlowyRenderPlugin _createRenderPlugin() => FlowyRenderPlugin(
|
FlowyRenderPlugin _createRenderPlugin() => FlowyRenderPlugin(
|
||||||
@ -116,4 +131,8 @@ class _FlowyEditorState extends State<FlowyEditor> {
|
|||||||
...widget.customBuilders,
|
...widget.customBuilders,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
void _scrollCallback() {
|
||||||
|
debugPrint('scrolling');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,65 @@
|
|||||||
|
import 'package:flutter/gestures.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
mixin FlowyScrollService<T extends StatefulWidget> on State<T> {
|
||||||
|
double get dy;
|
||||||
|
|
||||||
|
void scrollTo(double dy);
|
||||||
|
|
||||||
|
RenderObject? scrollRenderObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
class FlowyScroll extends StatefulWidget {
|
||||||
|
const FlowyScroll({
|
||||||
|
Key? key,
|
||||||
|
required this.child,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<FlowyScroll> createState() => _FlowyScrollState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FlowyScrollState extends State<FlowyScroll> with FlowyScrollService {
|
||||||
|
final _scrollController = ScrollController();
|
||||||
|
final _scrollViewKey = GlobalKey();
|
||||||
|
|
||||||
|
@override
|
||||||
|
double get dy => _scrollController.position.pixels;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Listener(
|
||||||
|
onPointerSignal: _onPointerSignal,
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
key: _scrollViewKey,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
controller: _scrollController,
|
||||||
|
child: widget.child,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void scrollTo(double dy) {
|
||||||
|
_scrollController.position.jumpTo(
|
||||||
|
dy.clamp(
|
||||||
|
_scrollController.position.minScrollExtent,
|
||||||
|
_scrollController.position.maxScrollExtent,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onPointerSignal(PointerSignalEvent event) {
|
||||||
|
if (event is PointerScrollEvent) {
|
||||||
|
final dy = (_scrollController.position.pixels + event.scrollDelta.dy);
|
||||||
|
scrollTo(dy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
RenderObject? scrollRenderObject() {
|
||||||
|
return _scrollViewKey.currentContext?.findRenderObject();
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,6 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/gestures.dart';
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
@ -99,102 +100,18 @@ class FlowySelection extends StatefulWidget {
|
|||||||
State<FlowySelection> createState() => _FlowySelectionState();
|
State<FlowySelection> createState() => _FlowySelectionState();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Because the flutter's [DoubleTapGestureRecognizer] will block the [TapGestureRecognizer]
|
|
||||||
/// for a while. So we need to implement our own GestureDetector.
|
|
||||||
@immutable
|
|
||||||
class _SelectionGestureDetector extends StatefulWidget {
|
|
||||||
const _SelectionGestureDetector(
|
|
||||||
{Key? key,
|
|
||||||
this.child,
|
|
||||||
this.onTapDown,
|
|
||||||
this.onDoubleTapDown,
|
|
||||||
this.onPanStart,
|
|
||||||
this.onPanUpdate,
|
|
||||||
this.onPanEnd})
|
|
||||||
: super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<_SelectionGestureDetector> createState() =>
|
|
||||||
_SelectionGestureDetectorState();
|
|
||||||
|
|
||||||
final Widget? child;
|
|
||||||
|
|
||||||
final GestureTapDownCallback? onTapDown;
|
|
||||||
final GestureTapDownCallback? onDoubleTapDown;
|
|
||||||
final GestureDragStartCallback? onPanStart;
|
|
||||||
final GestureDragUpdateCallback? onPanUpdate;
|
|
||||||
final GestureDragEndCallback? onPanEnd;
|
|
||||||
}
|
|
||||||
|
|
||||||
class _SelectionGestureDetectorState extends State<_SelectionGestureDetector> {
|
|
||||||
bool _isDoubleTap = false;
|
|
||||||
Timer? _doubleTapTimer;
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return RawGestureDetector(
|
|
||||||
behavior: HitTestBehavior.translucent,
|
|
||||||
gestures: {
|
|
||||||
PanGestureRecognizer:
|
|
||||||
GestureRecognizerFactoryWithHandlers<PanGestureRecognizer>(
|
|
||||||
() => PanGestureRecognizer(),
|
|
||||||
(recognizer) {
|
|
||||||
recognizer
|
|
||||||
..onStart = widget.onPanStart
|
|
||||||
..onUpdate = widget.onPanUpdate
|
|
||||||
..onEnd = widget.onPanEnd;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
TapGestureRecognizer:
|
|
||||||
GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
|
|
||||||
() => TapGestureRecognizer(),
|
|
||||||
(recognizer) {
|
|
||||||
recognizer.onTapDown = _tapDownDelegate;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
},
|
|
||||||
child: widget.child,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
_tapDownDelegate(TapDownDetails tapDownDetails) {
|
|
||||||
if (_isDoubleTap) {
|
|
||||||
_isDoubleTap = false;
|
|
||||||
_doubleTapTimer?.cancel();
|
|
||||||
_doubleTapTimer = null;
|
|
||||||
if (widget.onDoubleTapDown != null) {
|
|
||||||
widget.onDoubleTapDown!(tapDownDetails);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (widget.onTapDown != null) {
|
|
||||||
widget.onTapDown!(tapDownDetails);
|
|
||||||
}
|
|
||||||
|
|
||||||
_isDoubleTap = true;
|
|
||||||
_doubleTapTimer?.cancel();
|
|
||||||
_doubleTapTimer = Timer(kDoubleTapTimeout, () {
|
|
||||||
_isDoubleTap = false;
|
|
||||||
_doubleTapTimer = null;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_doubleTapTimer?.cancel();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _FlowySelectionState extends State<FlowySelection>
|
class _FlowySelectionState extends State<FlowySelection>
|
||||||
with FlowySelectionService, WidgetsBindingObserver {
|
with FlowySelectionService, WidgetsBindingObserver {
|
||||||
final _cursorKey = GlobalKey(debugLabel: 'cursor');
|
final _cursorKey = GlobalKey(debugLabel: 'cursor');
|
||||||
|
|
||||||
final List<OverlayEntry> _selectionOverlays = [];
|
final List<OverlayEntry> _selectionOverlays = [];
|
||||||
final List<OverlayEntry> _cursorOverlays = [];
|
final List<OverlayEntry> _cursorOverlays = [];
|
||||||
|
OverlayEntry? _debugOverlay;
|
||||||
|
|
||||||
/// [Pan] and [Tap] must be mutually exclusive.
|
/// [Pan] and [Tap] must be mutually exclusive.
|
||||||
/// Pan
|
/// Pan
|
||||||
Offset? panStartOffset;
|
Offset? panStartOffset;
|
||||||
|
double? panStartScrollDy;
|
||||||
Offset? panEndOffset;
|
Offset? panEndOffset;
|
||||||
|
|
||||||
/// Tap
|
/// Tap
|
||||||
@ -259,7 +176,7 @@ class _FlowySelectionState extends State<FlowySelection>
|
|||||||
@override
|
@override
|
||||||
void updateSelection(Selection selection) {
|
void updateSelection(Selection selection) {
|
||||||
_rects.clear();
|
_rects.clear();
|
||||||
_clearSelection();
|
clearSelection();
|
||||||
|
|
||||||
// cursor
|
// cursor
|
||||||
if (selection.isCollapsed) {
|
if (selection.isCollapsed) {
|
||||||
@ -273,7 +190,19 @@ class _FlowySelectionState extends State<FlowySelection>
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void clearSelection() {
|
void clearSelection() {
|
||||||
_clearSelection();
|
currentSelection = null;
|
||||||
|
currentSelectedNodes.value = [];
|
||||||
|
|
||||||
|
// clear selection
|
||||||
|
_selectionOverlays
|
||||||
|
..forEach((overlay) => overlay.remove())
|
||||||
|
..clear();
|
||||||
|
// clear cursors
|
||||||
|
_cursorOverlays
|
||||||
|
..forEach((overlay) => overlay.remove())
|
||||||
|
..clear();
|
||||||
|
// clear toolbar
|
||||||
|
editorState.service.toolbarService?.hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -325,7 +254,7 @@ class _FlowySelectionState extends State<FlowySelection>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (final child in node.children) {
|
for (final child in node.children) {
|
||||||
result.addAll(computeNodesInRange(child, start, end));
|
result.addAll(_computeNodesInRange(child, start, end));
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@ -411,12 +340,24 @@ class _FlowySelectionState extends State<FlowySelection>
|
|||||||
clearSelection();
|
clearSelection();
|
||||||
|
|
||||||
panStartOffset = details.globalPosition;
|
panStartOffset = details.globalPosition;
|
||||||
|
panStartScrollDy = editorState.service.scrollService?.dy;
|
||||||
|
|
||||||
|
debugPrint('[_onPanStart] panStartOffset = $panStartOffset');
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onPanUpdate(DragUpdateDetails details) {
|
void _onPanUpdate(DragUpdateDetails details) {
|
||||||
panEndOffset = details.globalPosition;
|
if (panStartOffset == null || panStartScrollDy == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
final nodes = getNodesInRange(panStartOffset!, panEndOffset!);
|
panEndOffset = details.globalPosition;
|
||||||
|
final dy = editorState.service.scrollService?.dy;
|
||||||
|
var panStartOffsetWithScrollDyGap = panStartOffset!;
|
||||||
|
if (dy != null) {
|
||||||
|
panStartOffsetWithScrollDyGap =
|
||||||
|
panStartOffsetWithScrollDyGap.translate(0, panStartScrollDy! - dy);
|
||||||
|
}
|
||||||
|
final nodes = getNodesInRange(panStartOffsetWithScrollDyGap, panEndOffset!);
|
||||||
if (nodes.isEmpty) {
|
if (nodes.isEmpty) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -427,40 +368,30 @@ class _FlowySelectionState extends State<FlowySelection>
|
|||||||
if (first != null && last != null) {
|
if (first != null && last != null) {
|
||||||
bool isDownward;
|
bool isDownward;
|
||||||
if (first == last) {
|
if (first == last) {
|
||||||
isDownward = panStartOffset!.dx < panEndOffset!.dx;
|
isDownward = panStartOffsetWithScrollDyGap.dx < panEndOffset!.dx;
|
||||||
} else {
|
} else {
|
||||||
isDownward = panStartOffset!.dy < panEndOffset!.dy;
|
isDownward = panStartOffsetWithScrollDyGap.dy < panEndOffset!.dy;
|
||||||
}
|
}
|
||||||
final start =
|
final start = first
|
||||||
first.getSelectionInRange(panStartOffset!, panEndOffset!).start;
|
.getSelectionInRange(panStartOffsetWithScrollDyGap, panEndOffset!)
|
||||||
final end = last.getSelectionInRange(panStartOffset!, panEndOffset!).end;
|
.start;
|
||||||
|
final end = last
|
||||||
|
.getSelectionInRange(panStartOffsetWithScrollDyGap, panEndOffset!)
|
||||||
|
.end;
|
||||||
final selection = Selection(
|
final selection = Selection(
|
||||||
start: isDownward ? start : end, end: isDownward ? end : start);
|
start: isDownward ? start : end, end: isDownward ? end : start);
|
||||||
debugPrint('[_onPanUpdate] isDownward = $isDownward, $selection');
|
debugPrint('[_onPanUpdate] isDownward = $isDownward, $selection');
|
||||||
editorState.service.selectionService.updateSelection(selection);
|
editorState.service.selectionService.updateSelection(selection);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_scrollUpOrDownIfNeeded(panEndOffset!);
|
||||||
|
_showDebugLayerIfNeeded();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onPanEnd(DragEndDetails details) {
|
void _onPanEnd(DragEndDetails details) {
|
||||||
// do nothing
|
// do nothing
|
||||||
}
|
}
|
||||||
|
|
||||||
void _clearSelection() {
|
|
||||||
currentSelection = null;
|
|
||||||
currentSelectedNodes.value = [];
|
|
||||||
|
|
||||||
// clear selection
|
|
||||||
_selectionOverlays
|
|
||||||
..forEach((overlay) => overlay.remove())
|
|
||||||
..clear();
|
|
||||||
// clear cursors
|
|
||||||
_cursorOverlays
|
|
||||||
..forEach((overlay) => overlay.remove())
|
|
||||||
..clear();
|
|
||||||
// clear toolbar
|
|
||||||
editorState.service.toolbarService?.hide();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _updateSelection(Selection selection) {
|
void _updateSelection(Selection selection) {
|
||||||
final nodes =
|
final nodes =
|
||||||
_selectedNodesInSelection(editorState.document.root, selection);
|
_selectedNodesInSelection(editorState.document.root, selection);
|
||||||
@ -554,12 +485,12 @@ class _FlowySelectionState extends State<FlowySelection>
|
|||||||
if (rect != null) {
|
if (rect != null) {
|
||||||
_rects.add(_transformRectToGlobal(selectable!, rect));
|
_rects.add(_transformRectToGlobal(selectable!, rect));
|
||||||
final cursor = OverlayEntry(
|
final cursor = OverlayEntry(
|
||||||
builder: ((context) => CursorWidget(
|
builder: (context) => CursorWidget(
|
||||||
key: _cursorKey,
|
key: _cursorKey,
|
||||||
rect: rect,
|
rect: rect,
|
||||||
color: widget.cursorColor,
|
color: widget.cursorColor,
|
||||||
layerLink: node.layerLink,
|
layerLink: node.layerLink,
|
||||||
)),
|
),
|
||||||
);
|
);
|
||||||
_cursorOverlays.add(cursor);
|
_cursorOverlays.add(cursor);
|
||||||
Overlay.of(context)?.insertAll(_cursorOverlays);
|
Overlay.of(context)?.insertAll(_cursorOverlays);
|
||||||
@ -584,4 +515,139 @@ class _FlowySelectionState extends State<FlowySelection>
|
|||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _scrollUpOrDownIfNeeded(Offset offset) {
|
||||||
|
final dy = editorState.service.scrollService?.dy;
|
||||||
|
if (dy == null) {
|
||||||
|
assert(false, 'Dy could not be null');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final topLimit = MediaQuery.of(context).size.height * 0.2;
|
||||||
|
final bottomLimit = MediaQuery.of(context).size.height * 0.8;
|
||||||
|
|
||||||
|
/// TODO: It is necessary to calculate the relative speed
|
||||||
|
/// according to the gap and move forward more gently.
|
||||||
|
final distance = 10.0;
|
||||||
|
if (offset.dy <= topLimit) {
|
||||||
|
// up
|
||||||
|
editorState.service.scrollService?.scrollTo(dy - distance);
|
||||||
|
} else if (offset.dy >= bottomLimit) {
|
||||||
|
//down
|
||||||
|
editorState.service.scrollService?.scrollTo(dy + distance);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showDebugLayerIfNeeded() {
|
||||||
|
// remove false to show debug overlay.
|
||||||
|
if (kDebugMode && false) {
|
||||||
|
_debugOverlay?.remove();
|
||||||
|
if (panStartOffset != null) {
|
||||||
|
_debugOverlay = OverlayEntry(
|
||||||
|
builder: (context) => Positioned.fromRect(
|
||||||
|
rect: Rect.fromPoints(
|
||||||
|
panStartOffset?.translate(
|
||||||
|
0,
|
||||||
|
-(editorState.service.scrollService!.dy -
|
||||||
|
panStartScrollDy!),
|
||||||
|
) ??
|
||||||
|
Offset.zero,
|
||||||
|
panEndOffset ?? Offset.zero)
|
||||||
|
.translate(0, 0),
|
||||||
|
child: Container(
|
||||||
|
color: Colors.red.withOpacity(0.2),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
Overlay.of(context)?.insert(_debugOverlay!);
|
||||||
|
} else {
|
||||||
|
_debugOverlay = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Because the flutter's [DoubleTapGestureRecognizer] will block the [TapGestureRecognizer]
|
||||||
|
/// for a while. So we need to implement our own GestureDetector.
|
||||||
|
@immutable
|
||||||
|
class _SelectionGestureDetector extends StatefulWidget {
|
||||||
|
const _SelectionGestureDetector(
|
||||||
|
{Key? key,
|
||||||
|
this.child,
|
||||||
|
this.onTapDown,
|
||||||
|
this.onDoubleTapDown,
|
||||||
|
this.onPanStart,
|
||||||
|
this.onPanUpdate,
|
||||||
|
this.onPanEnd})
|
||||||
|
: super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_SelectionGestureDetector> createState() =>
|
||||||
|
_SelectionGestureDetectorState();
|
||||||
|
|
||||||
|
final Widget? child;
|
||||||
|
|
||||||
|
final GestureTapDownCallback? onTapDown;
|
||||||
|
final GestureTapDownCallback? onDoubleTapDown;
|
||||||
|
final GestureDragStartCallback? onPanStart;
|
||||||
|
final GestureDragUpdateCallback? onPanUpdate;
|
||||||
|
final GestureDragEndCallback? onPanEnd;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SelectionGestureDetectorState extends State<_SelectionGestureDetector> {
|
||||||
|
bool _isDoubleTap = false;
|
||||||
|
Timer? _doubleTapTimer;
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return RawGestureDetector(
|
||||||
|
behavior: HitTestBehavior.translucent,
|
||||||
|
gestures: {
|
||||||
|
PanGestureRecognizer:
|
||||||
|
GestureRecognizerFactoryWithHandlers<PanGestureRecognizer>(
|
||||||
|
() => PanGestureRecognizer(),
|
||||||
|
(recognizer) {
|
||||||
|
recognizer
|
||||||
|
..onStart = widget.onPanStart
|
||||||
|
..onUpdate = widget.onPanUpdate
|
||||||
|
..onEnd = widget.onPanEnd;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
TapGestureRecognizer:
|
||||||
|
GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
|
||||||
|
() => TapGestureRecognizer(),
|
||||||
|
(recognizer) {
|
||||||
|
recognizer.onTapDown = _tapDownDelegate;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
child: widget.child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_tapDownDelegate(TapDownDetails tapDownDetails) {
|
||||||
|
if (_isDoubleTap) {
|
||||||
|
_isDoubleTap = false;
|
||||||
|
_doubleTapTimer?.cancel();
|
||||||
|
_doubleTapTimer = null;
|
||||||
|
if (widget.onDoubleTapDown != null) {
|
||||||
|
widget.onDoubleTapDown!(tapDownDetails);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (widget.onTapDown != null) {
|
||||||
|
widget.onTapDown!(tapDownDetails);
|
||||||
|
}
|
||||||
|
|
||||||
|
_isDoubleTap = true;
|
||||||
|
_doubleTapTimer?.cancel();
|
||||||
|
_doubleTapTimer = Timer(kDoubleTapTimeout, () {
|
||||||
|
_isDoubleTap = false;
|
||||||
|
_doubleTapTimer = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_doubleTapTimer?.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:flowy_editor/service/keyboard_service.dart';
|
import 'package:flowy_editor/service/keyboard_service.dart';
|
||||||
import 'package:flowy_editor/service/render_plugin_service.dart';
|
import 'package:flowy_editor/service/render_plugin_service.dart';
|
||||||
import 'package:flowy_editor/service/toolbar_service.dart';
|
import 'package:flowy_editor/service/scroll_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:flowy_editor/service/toolbar_service.dart';
|
||||||
|
|
||||||
class FlowyService {
|
class FlowyService {
|
||||||
// selection service
|
// selection service
|
||||||
@ -31,10 +33,20 @@ class FlowyService {
|
|||||||
|
|
||||||
// toolbar service
|
// toolbar service
|
||||||
final toolbarServiceKey = GlobalKey(debugLabel: 'flowy_toolbar_service');
|
final toolbarServiceKey = GlobalKey(debugLabel: 'flowy_toolbar_service');
|
||||||
ToolbarService? get toolbarService {
|
FlowyToolbarService? get toolbarService {
|
||||||
if (toolbarServiceKey.currentState != null &&
|
if (toolbarServiceKey.currentState != null &&
|
||||||
toolbarServiceKey.currentState is ToolbarService) {
|
toolbarServiceKey.currentState is FlowyToolbarService) {
|
||||||
return toolbarServiceKey.currentState! as ToolbarService;
|
return toolbarServiceKey.currentState! as FlowyToolbarService;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// scroll service
|
||||||
|
final scrollServiceKey = GlobalKey(debugLabel: 'flowy_scroll_service');
|
||||||
|
FlowyScrollService? get scrollService {
|
||||||
|
if (scrollServiceKey.currentState != null &&
|
||||||
|
scrollServiceKey.currentState is FlowyScrollService) {
|
||||||
|
return scrollServiceKey.currentState! as FlowyScrollService;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import 'package:flowy_editor/flowy_editor.dart';
|
|
||||||
import 'package:flowy_editor/render/selection/toolbar_widget.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
mixin ToolbarService {
|
import 'package:flowy_editor/flowy_editor.dart';
|
||||||
|
import 'package:flowy_editor/render/selection/toolbar_widget.dart';
|
||||||
|
|
||||||
|
mixin FlowyToolbarService {
|
||||||
/// Show the toolbar widget beside the offset.
|
/// Show the toolbar widget beside the offset.
|
||||||
void showInOffset(Offset offset, LayerLink layerLink);
|
void showInOffset(Offset offset, LayerLink layerLink);
|
||||||
|
|
||||||
@ -24,7 +25,7 @@ class FlowyToolbar extends StatefulWidget {
|
|||||||
State<FlowyToolbar> createState() => _FlowyToolbarState();
|
State<FlowyToolbar> createState() => _FlowyToolbarState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _FlowyToolbarState extends State<FlowyToolbar> with ToolbarService {
|
class _FlowyToolbarState extends State<FlowyToolbar> with FlowyToolbarService {
|
||||||
OverlayEntry? _toolbarOverlay;
|
OverlayEntry? _toolbarOverlay;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
Loading…
Reference in New Issue
Block a user