|
@@ -1,81 +1,79 @@
|
|
-import 'dart:async';
|
|
|
|
-
|
|
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter/foundation.dart';
|
|
-import 'package:flutter/gestures.dart';
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/material.dart';
|
|
|
|
|
|
import 'package:flowy_editor/src/document/node.dart';
|
|
import 'package:flowy_editor/src/document/node.dart';
|
|
import 'package:flowy_editor/src/document/node_iterator.dart';
|
|
import 'package:flowy_editor/src/document/node_iterator.dart';
|
|
import 'package:flowy_editor/src/document/position.dart';
|
|
import 'package:flowy_editor/src/document/position.dart';
|
|
import 'package:flowy_editor/src/document/selection.dart';
|
|
import 'package:flowy_editor/src/document/selection.dart';
|
|
-import 'package:flowy_editor/src/document/state_tree.dart';
|
|
|
|
import 'package:flowy_editor/src/editor_state.dart';
|
|
import 'package:flowy_editor/src/editor_state.dart';
|
|
import 'package:flowy_editor/src/extensions/node_extensions.dart';
|
|
import 'package:flowy_editor/src/extensions/node_extensions.dart';
|
|
|
|
+import 'package:flowy_editor/src/extensions/object_extensions.dart';
|
|
|
|
+import 'package:flowy_editor/src/extensions/path_extensions.dart';
|
|
import 'package:flowy_editor/src/render/selection/cursor_widget.dart';
|
|
import 'package:flowy_editor/src/render/selection/cursor_widget.dart';
|
|
import 'package:flowy_editor/src/render/selection/selectable.dart';
|
|
import 'package:flowy_editor/src/render/selection/selectable.dart';
|
|
import 'package:flowy_editor/src/render/selection/selection_widget.dart';
|
|
import 'package:flowy_editor/src/render/selection/selection_widget.dart';
|
|
-
|
|
|
|
-/// Process selection and cursor
|
|
|
|
-mixin FlowySelectionService<T extends StatefulWidget> on State<T> {
|
|
|
|
- /// Returns the current [Selection]
|
|
|
|
|
|
+import 'package:flowy_editor/src/service/selection/selection_gesture.dart';
|
|
|
|
+
|
|
|
|
+/// [FlowySelectionService] is responsible for processing
|
|
|
|
+/// the [Selection] changes and updates.
|
|
|
|
+///
|
|
|
|
+/// Usually, this service can be obtained by the following code.
|
|
|
|
+/// ```dart
|
|
|
|
+/// final selectionService = editorState.service.selectionService;
|
|
|
|
+///
|
|
|
|
+/// /** get current selection value*/
|
|
|
|
+/// final selection = selectionService.currentSelection.value;
|
|
|
|
+///
|
|
|
|
+/// /** get current selected nodes*/
|
|
|
|
+/// final nodes = selectionService.currentSelectedNodes;
|
|
|
|
+/// ```
|
|
|
|
+///
|
|
|
|
+abstract class FlowySelectionService {
|
|
|
|
+ /// The current [Selection] in editor.
|
|
|
|
+ ///
|
|
|
|
+ /// The value is null if there is no nodes are selected.
|
|
ValueNotifier<Selection?> get currentSelection;
|
|
ValueNotifier<Selection?> get currentSelection;
|
|
|
|
|
|
- /// Returns the current selected [Node]s.
|
|
|
|
|
|
+ /// The current selected [Node]s in editor.
|
|
|
|
+ ///
|
|
|
|
+ /// The order of the result is determined according to the [currentSelection].
|
|
|
|
+ /// The result are ordered from back to front if the selection is forward.
|
|
|
|
+ /// The result are ordered from front to back if the selection is backward.
|
|
///
|
|
///
|
|
- /// The order of the return is determined according to the selected order.
|
|
|
|
|
|
+ /// For example, Here is an array of selected nodes, [n1, n2, n3].
|
|
|
|
+ /// The result will be [n3, n2, n1] if the selection is forward,
|
|
|
|
+ /// and [n1, n2, n3] if the selection is backward.
|
|
|
|
+ ///
|
|
|
|
+ /// Returns empty result if there is no nodes are selected.
|
|
List<Node> get currentSelectedNodes;
|
|
List<Node> get currentSelectedNodes;
|
|
|
|
|
|
- /// Update the selection or cursor.
|
|
|
|
|
|
+ /// Updates the selection.
|
|
///
|
|
///
|
|
- /// If selection is collapsed, this method will
|
|
|
|
- /// update the position of the cursor.
|
|
|
|
- /// Otherwise, will update the selection.
|
|
|
|
|
|
+ /// The editor will update selection area and toolbar area
|
|
|
|
+ /// if the [selection] is not collapsed,
|
|
|
|
+ /// otherwise, will update the cursor area.
|
|
void updateSelection(Selection selection);
|
|
void updateSelection(Selection selection);
|
|
|
|
|
|
- /// Clear the selection or cursor.
|
|
|
|
|
|
+ /// Clears the selection area, cursor area and the popup list area.
|
|
void clearSelection();
|
|
void clearSelection();
|
|
|
|
|
|
- /// ------------------ Selection ------------------------
|
|
|
|
-
|
|
|
|
- List<Rect> rects();
|
|
|
|
-
|
|
|
|
- Position? hitTest(Offset? offset);
|
|
|
|
-
|
|
|
|
- ///
|
|
|
|
|
|
+ /// Returns the [Node]s in [Selection].
|
|
List<Node> getNodesInSelection(Selection selection);
|
|
List<Node> getNodesInSelection(Selection selection);
|
|
|
|
|
|
- /// ------------------ Selection ------------------------
|
|
|
|
-
|
|
|
|
- /// ------------------ Offset ------------------------
|
|
|
|
-
|
|
|
|
- /// Return the [Node] or [Null] in single selection.
|
|
|
|
|
|
+ /// Returns the [Node] containing to the [offset].
|
|
///
|
|
///
|
|
- /// [offset] is under the global coordinate system.
|
|
|
|
|
|
+ /// [offset] must be under the global coordinate system.
|
|
Node? getNodeInOffset(Offset offset);
|
|
Node? getNodeInOffset(Offset offset);
|
|
|
|
|
|
- /// Returns selected [Node]s. Empty list would be returned
|
|
|
|
- /// if no nodes are in range.
|
|
|
|
|
|
+ /// Returns the [Position] closest to the [offset].
|
|
///
|
|
///
|
|
|
|
+ /// Returns null if there is no nodes are selected.
|
|
///
|
|
///
|
|
- /// [start] and [end] are under the global coordinate system.
|
|
|
|
- ///
|
|
|
|
- List<Node> getNodeInRange(Offset start, Offset end);
|
|
|
|
|
|
+ /// [offset] must be under the global coordinate system.
|
|
|
|
+ Position? getPositionInOffset(Offset offset);
|
|
|
|
|
|
- /// Return [bool] to identify the [Node] is in Range or not.
|
|
|
|
- ///
|
|
|
|
- /// [start] and [end] are under the global coordinate system.
|
|
|
|
- bool isNodeInRange(
|
|
|
|
- Node node,
|
|
|
|
- Offset start,
|
|
|
|
- Offset end,
|
|
|
|
- );
|
|
|
|
-
|
|
|
|
- /// Return [bool] to identify the [Node] contains [Offset] or not.
|
|
|
|
- ///
|
|
|
|
- /// [offset] is under the global coordinate system.
|
|
|
|
- bool isNodeInOffset(Node node, Offset offset);
|
|
|
|
-
|
|
|
|
- /// ------------------ Offset ------------------------
|
|
|
|
|
|
+ /// The current selection areas's rect in editor.
|
|
|
|
+ List<Rect> get selectionRects;
|
|
}
|
|
}
|
|
|
|
|
|
class FlowySelection extends StatefulWidget {
|
|
class FlowySelection extends StatefulWidget {
|
|
@@ -97,41 +95,29 @@ class FlowySelection extends StatefulWidget {
|
|
}
|
|
}
|
|
|
|
|
|
class _FlowySelectionState extends State<FlowySelection>
|
|
class _FlowySelectionState extends State<FlowySelection>
|
|
- with FlowySelectionService, WidgetsBindingObserver {
|
|
|
|
|
|
+ with WidgetsBindingObserver
|
|
|
|
+ implements FlowySelectionService {
|
|
final _cursorKey = GlobalKey(debugLabel: 'cursor');
|
|
final _cursorKey = GlobalKey(debugLabel: 'cursor');
|
|
|
|
|
|
- final List<OverlayEntry> _selectionOverlays = [];
|
|
|
|
- final List<OverlayEntry> _cursorOverlays = [];
|
|
|
|
|
|
+ @override
|
|
|
|
+ final List<Rect> selectionRects = [];
|
|
|
|
+ final List<OverlayEntry> _selectionAreas = [];
|
|
|
|
+ final List<OverlayEntry> _cursorAreas = [];
|
|
|
|
+
|
|
OverlayEntry? _debugOverlay;
|
|
OverlayEntry? _debugOverlay;
|
|
|
|
|
|
- /// [Pan] and [Tap] must be mutually exclusive.
|
|
|
|
/// Pan
|
|
/// Pan
|
|
- Offset? panStartOffset;
|
|
|
|
- double? panStartScrollDy;
|
|
|
|
- Offset? panEndOffset;
|
|
|
|
-
|
|
|
|
- /// Tap
|
|
|
|
- Offset? tapOffset;
|
|
|
|
-
|
|
|
|
- final List<Rect> _rects = [];
|
|
|
|
|
|
+ Offset? _panStartOffset;
|
|
|
|
+ double? _panStartScrollDy;
|
|
|
|
|
|
EditorState get editorState => widget.editorState;
|
|
EditorState get editorState => widget.editorState;
|
|
|
|
|
|
- @override
|
|
|
|
- ValueNotifier<Selection?> currentSelection = ValueNotifier(null);
|
|
|
|
-
|
|
|
|
- @override
|
|
|
|
- List<Node> currentSelectedNodes = [];
|
|
|
|
-
|
|
|
|
- @override
|
|
|
|
- List<Node> getNodesInSelection(Selection selection) =>
|
|
|
|
- _selectedNodesInSelection(editorState.document, selection);
|
|
|
|
-
|
|
|
|
@override
|
|
@override
|
|
void initState() {
|
|
void initState() {
|
|
super.initState();
|
|
super.initState();
|
|
|
|
|
|
WidgetsBinding.instance.addObserver(this);
|
|
WidgetsBinding.instance.addObserver(this);
|
|
|
|
+ currentSelection.addListener(_onSelectionChange);
|
|
}
|
|
}
|
|
|
|
|
|
@override
|
|
@override
|
|
@@ -148,13 +134,14 @@ class _FlowySelectionState extends State<FlowySelection>
|
|
void dispose() {
|
|
void dispose() {
|
|
clearSelection();
|
|
clearSelection();
|
|
WidgetsBinding.instance.removeObserver(this);
|
|
WidgetsBinding.instance.removeObserver(this);
|
|
|
|
+ currentSelection.removeListener(_onSelectionChange);
|
|
|
|
|
|
super.dispose();
|
|
super.dispose();
|
|
}
|
|
}
|
|
|
|
|
|
@override
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
Widget build(BuildContext context) {
|
|
- return _SelectionGestureDetector(
|
|
|
|
|
|
+ return SelectionGestureDetector(
|
|
onPanStart: _onPanStart,
|
|
onPanStart: _onPanStart,
|
|
onPanUpdate: _onPanUpdate,
|
|
onPanUpdate: _onPanUpdate,
|
|
onPanEnd: _onPanEnd,
|
|
onPanEnd: _onPanEnd,
|
|
@@ -166,23 +153,48 @@ class _FlowySelectionState extends State<FlowySelection>
|
|
}
|
|
}
|
|
|
|
|
|
@override
|
|
@override
|
|
- List<Rect> rects() {
|
|
|
|
- return _rects;
|
|
|
|
|
|
+ ValueNotifier<Selection?> currentSelection = ValueNotifier(null);
|
|
|
|
+
|
|
|
|
+ @override
|
|
|
|
+ List<Node> currentSelectedNodes = [];
|
|
|
|
+
|
|
|
|
+ @override
|
|
|
|
+ List<Node> getNodesInSelection(Selection selection) {
|
|
|
|
+ final start =
|
|
|
|
+ selection.isBackward ? selection.start.path : selection.end.path;
|
|
|
|
+ final end =
|
|
|
|
+ selection.isBackward ? selection.end.path : selection.start.path;
|
|
|
|
+ assert(start <= end);
|
|
|
|
+ final startNode = editorState.document.nodeAtPath(start);
|
|
|
|
+ final endNode = editorState.document.nodeAtPath(end);
|
|
|
|
+ if (startNode != null && endNode != null) {
|
|
|
|
+ final nodes =
|
|
|
|
+ NodeIterator(editorState.document, startNode, endNode).toList();
|
|
|
|
+ if (selection.isBackward) {
|
|
|
|
+ return nodes;
|
|
|
|
+ } else {
|
|
|
|
+ return nodes.reversed.toList(growable: false);
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ return [];
|
|
}
|
|
}
|
|
|
|
|
|
@override
|
|
@override
|
|
void updateSelection(Selection selection) {
|
|
void updateSelection(Selection selection) {
|
|
- _rects.clear();
|
|
|
|
|
|
+ selectionRects.clear();
|
|
clearSelection();
|
|
clearSelection();
|
|
|
|
|
|
- // cursor
|
|
|
|
if (selection.isCollapsed) {
|
|
if (selection.isCollapsed) {
|
|
- debugPrint('Update cursor');
|
|
|
|
- _updateCursor(selection.start);
|
|
|
|
|
|
+ /// updates cursor area.
|
|
|
|
+ debugPrint('updating cursor');
|
|
|
|
+ _updateCursorAreas(selection.start);
|
|
} else {
|
|
} else {
|
|
- debugPrint('Update selection');
|
|
|
|
- _updateSelection(selection);
|
|
|
|
|
|
+ // updates selection area.
|
|
|
|
+ debugPrint('updating selection');
|
|
|
|
+ _updateSelectionAreas(selection);
|
|
}
|
|
}
|
|
|
|
+
|
|
|
|
+ currentSelection.value = selection;
|
|
}
|
|
}
|
|
|
|
|
|
@override
|
|
@override
|
|
@@ -190,224 +202,172 @@ class _FlowySelectionState extends State<FlowySelection>
|
|
currentSelectedNodes = [];
|
|
currentSelectedNodes = [];
|
|
currentSelection.value = null;
|
|
currentSelection.value = null;
|
|
|
|
|
|
- // clear selection
|
|
|
|
- _selectionOverlays
|
|
|
|
|
|
+ // clear selection areas
|
|
|
|
+ _selectionAreas
|
|
..forEach((overlay) => overlay.remove())
|
|
..forEach((overlay) => overlay.remove())
|
|
..clear();
|
|
..clear();
|
|
- // clear cursors
|
|
|
|
- _cursorOverlays
|
|
|
|
|
|
+ // clear cursor areas
|
|
|
|
+ _cursorAreas
|
|
..forEach((overlay) => overlay.remove())
|
|
..forEach((overlay) => overlay.remove())
|
|
..clear();
|
|
..clear();
|
|
- // clear toolbar
|
|
|
|
|
|
+ // hide toolbar
|
|
editorState.service.toolbarService?.hide();
|
|
editorState.service.toolbarService?.hide();
|
|
}
|
|
}
|
|
|
|
|
|
@override
|
|
@override
|
|
Node? getNodeInOffset(Offset offset) {
|
|
Node? getNodeInOffset(Offset offset) {
|
|
- return _lowerBoundInDocument(offset);
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- @override
|
|
|
|
- List<Node> getNodeInRange(Offset start, Offset end) {
|
|
|
|
- final startNode = _lowerBoundInDocument(start);
|
|
|
|
- final endNode = _upperBoundInDocument(end);
|
|
|
|
- return NodeIterator(editorState.document, startNode, endNode).toList();
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- @override
|
|
|
|
- bool isNodeInOffset(Node node, Offset offset) {
|
|
|
|
- final renderBox = node.renderBox;
|
|
|
|
- if (renderBox != null) {
|
|
|
|
- final boxOffset = renderBox.localToGlobal(Offset.zero);
|
|
|
|
- final boxRect = boxOffset & renderBox.size;
|
|
|
|
- return boxRect.contains(offset);
|
|
|
|
- }
|
|
|
|
- return false;
|
|
|
|
|
|
+ final sortedNodes =
|
|
|
|
+ editorState.document.root.children.toList(growable: false);
|
|
|
|
+ return _getNodeInOffset(
|
|
|
|
+ sortedNodes,
|
|
|
|
+ offset,
|
|
|
|
+ 0,
|
|
|
|
+ sortedNodes.length - 1,
|
|
|
|
+ );
|
|
}
|
|
}
|
|
|
|
|
|
@override
|
|
@override
|
|
- bool isNodeInRange(Node node, Offset start, Offset end) {
|
|
|
|
- final renderBox = node.renderBox;
|
|
|
|
- if (renderBox != null) {
|
|
|
|
- final rect = Rect.fromPoints(start, end);
|
|
|
|
- final boxOffset = renderBox.localToGlobal(Offset.zero);
|
|
|
|
- final boxRect = boxOffset & renderBox.size;
|
|
|
|
- return rect.overlaps(boxRect);
|
|
|
|
- }
|
|
|
|
- return false;
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- void _onDoubleTapDown(TapDownDetails details) {
|
|
|
|
- final offset = details.globalPosition;
|
|
|
|
|
|
+ Position? getPositionInOffset(Offset offset) {
|
|
final node = getNodeInOffset(offset);
|
|
final node = getNodeInOffset(offset);
|
|
- if (node == null) {
|
|
|
|
- editorState.updateCursorSelection(null);
|
|
|
|
- return;
|
|
|
|
- }
|
|
|
|
- final selectable = node.selectable;
|
|
|
|
|
|
+ final selectable = node?.selectable;
|
|
if (selectable == null) {
|
|
if (selectable == null) {
|
|
- editorState.updateCursorSelection(null);
|
|
|
|
- return;
|
|
|
|
- }
|
|
|
|
- editorState
|
|
|
|
- .updateCursorSelection(selectable.getWorldBoundaryInOffset(offset));
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- void _onTripleTapDown(TapDownDetails details) {
|
|
|
|
- final offset = details.globalPosition;
|
|
|
|
- final node = getNodeInOffset(offset);
|
|
|
|
- if (node == null) {
|
|
|
|
- editorState.updateCursorSelection(null);
|
|
|
|
- return;
|
|
|
|
- }
|
|
|
|
- Selection selection;
|
|
|
|
- if (node is TextNode) {
|
|
|
|
- final textLen = node.delta.length;
|
|
|
|
- selection = Selection(
|
|
|
|
- start: Position(path: node.path, offset: 0),
|
|
|
|
- end: Position(path: node.path, offset: textLen));
|
|
|
|
- } else {
|
|
|
|
- selection = Selection.collapsed(Position(path: node.path, offset: 0));
|
|
|
|
|
|
+ clearSelection();
|
|
|
|
+ return null;
|
|
}
|
|
}
|
|
- editorState.updateCursorSelection(selection);
|
|
|
|
|
|
+ return selectable.getPositionInOffset(offset);
|
|
}
|
|
}
|
|
|
|
|
|
void _onTapDown(TapDownDetails details) {
|
|
void _onTapDown(TapDownDetails details) {
|
|
// clear old state.
|
|
// clear old state.
|
|
- panStartOffset = null;
|
|
|
|
- panEndOffset = null;
|
|
|
|
-
|
|
|
|
- tapOffset = details.globalPosition;
|
|
|
|
|
|
+ _panStartOffset = null;
|
|
|
|
|
|
- final position = hitTest(tapOffset);
|
|
|
|
|
|
+ final position = getPositionInOffset(details.globalPosition);
|
|
if (position == null) {
|
|
if (position == null) {
|
|
return;
|
|
return;
|
|
}
|
|
}
|
|
final selection = Selection.collapsed(position);
|
|
final selection = Selection.collapsed(position);
|
|
- editorState.updateCursorSelection(selection);
|
|
|
|
|
|
+ updateSelection(selection);
|
|
|
|
|
|
- editorState.service.keyboardService?.enable();
|
|
|
|
- editorState.service.scrollService?.enable();
|
|
|
|
|
|
+ _enableInteraction();
|
|
|
|
+
|
|
|
|
+ _showDebugLayerIfNeeded(offset: details.globalPosition);
|
|
}
|
|
}
|
|
|
|
|
|
- @override
|
|
|
|
- Position? hitTest(Offset? offset) {
|
|
|
|
- if (offset == null) {
|
|
|
|
- editorState.updateCursorSelection(null);
|
|
|
|
- return null;
|
|
|
|
- }
|
|
|
|
|
|
+ void _onDoubleTapDown(TapDownDetails details) {
|
|
|
|
+ final offset = details.globalPosition;
|
|
final node = getNodeInOffset(offset);
|
|
final node = getNodeInOffset(offset);
|
|
- if (node == null) {
|
|
|
|
- editorState.updateCursorSelection(null);
|
|
|
|
- return null;
|
|
|
|
|
|
+ final selection = node?.selectable?.getWorldBoundaryInOffset(offset);
|
|
|
|
+ if (selection == null) {
|
|
|
|
+ clearSelection();
|
|
|
|
+ return;
|
|
}
|
|
}
|
|
- final selectable = node.selectable;
|
|
|
|
|
|
+ updateSelection(selection);
|
|
|
|
+
|
|
|
|
+ _enableInteraction();
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ void _onTripleTapDown(TapDownDetails details) {
|
|
|
|
+ final offset = details.globalPosition;
|
|
|
|
+ final node = getNodeInOffset(offset);
|
|
|
|
+ final selectable = node?.selectable;
|
|
if (selectable == null) {
|
|
if (selectable == null) {
|
|
- editorState.updateCursorSelection(null);
|
|
|
|
- return null;
|
|
|
|
|
|
+ clearSelection();
|
|
|
|
+ return;
|
|
}
|
|
}
|
|
- return selectable.getPositionInOffset(offset);
|
|
|
|
|
|
+ Selection selection = Selection(
|
|
|
|
+ start: selectable.start(),
|
|
|
|
+ end: selectable.end(),
|
|
|
|
+ );
|
|
|
|
+ updateSelection(selection);
|
|
|
|
+
|
|
|
|
+ _enableInteraction();
|
|
}
|
|
}
|
|
|
|
|
|
void _onPanStart(DragStartDetails details) {
|
|
void _onPanStart(DragStartDetails details) {
|
|
- // clear old state.
|
|
|
|
- panEndOffset = null;
|
|
|
|
- tapOffset = null;
|
|
|
|
clearSelection();
|
|
clearSelection();
|
|
|
|
|
|
- panStartOffset = details.globalPosition;
|
|
|
|
- panStartScrollDy = editorState.service.scrollService?.dy;
|
|
|
|
|
|
+ _panStartOffset = details.globalPosition;
|
|
|
|
+ _panStartScrollDy = editorState.service.scrollService?.dy;
|
|
|
|
|
|
- debugPrint('[_onPanStart] panStartOffset = $panStartOffset');
|
|
|
|
|
|
+ _enableInteraction();
|
|
}
|
|
}
|
|
|
|
|
|
void _onPanUpdate(DragUpdateDetails details) {
|
|
void _onPanUpdate(DragUpdateDetails details) {
|
|
- if (panStartOffset == null || panStartScrollDy == null) {
|
|
|
|
|
|
+ if (_panStartOffset == null || _panStartScrollDy == null) {
|
|
return;
|
|
return;
|
|
}
|
|
}
|
|
|
|
|
|
- editorState.service.keyboardService?.enable();
|
|
|
|
- editorState.service.scrollService?.enable();
|
|
|
|
|
|
+ _enableInteraction();
|
|
|
|
|
|
- panEndOffset = details.globalPosition;
|
|
|
|
|
|
+ final panEndOffset = details.globalPosition;
|
|
final dy = editorState.service.scrollService?.dy;
|
|
final dy = editorState.service.scrollService?.dy;
|
|
- var panStartOffsetWithScrollDyGap = panStartOffset!;
|
|
|
|
- if (dy != null) {
|
|
|
|
- panStartOffsetWithScrollDyGap =
|
|
|
|
- panStartOffsetWithScrollDyGap.translate(0, panStartScrollDy! - dy);
|
|
|
|
- }
|
|
|
|
|
|
+ final panStartOffset = dy == null
|
|
|
|
+ ? _panStartOffset!
|
|
|
|
+ : _panStartOffset!.translate(0, _panStartScrollDy! - dy);
|
|
|
|
|
|
- final first =
|
|
|
|
- _lowerBoundInDocument(panStartOffsetWithScrollDyGap).selectable;
|
|
|
|
- final last = _upperBoundInDocument(panEndOffset!).selectable;
|
|
|
|
|
|
+ final first = getNodeInOffset(panStartOffset)?.selectable;
|
|
|
|
+ final last = getNodeInOffset(panEndOffset)?.selectable;
|
|
|
|
|
|
// compute the selection in range.
|
|
// compute the selection in range.
|
|
if (first != null && last != null) {
|
|
if (first != null && last != null) {
|
|
- bool isDownward;
|
|
|
|
- if (first == last) {
|
|
|
|
- isDownward = panStartOffsetWithScrollDyGap.dx < panEndOffset!.dx;
|
|
|
|
- } else {
|
|
|
|
- isDownward = panStartOffsetWithScrollDyGap.dy < panEndOffset!.dy;
|
|
|
|
- }
|
|
|
|
- final start = first
|
|
|
|
- .getSelectionInRange(panStartOffsetWithScrollDyGap, panEndOffset!)
|
|
|
|
- .start;
|
|
|
|
- final end = last
|
|
|
|
- .getSelectionInRange(panStartOffsetWithScrollDyGap, panEndOffset!)
|
|
|
|
- .end;
|
|
|
|
- final selection = Selection(
|
|
|
|
- start: isDownward ? start : end, end: isDownward ? end : start);
|
|
|
|
|
|
+ bool isDownward = (identical(first, last))
|
|
|
|
+ ? panStartOffset.dx < panEndOffset.dx
|
|
|
|
+ : panStartOffset.dy < panEndOffset.dy;
|
|
|
|
+ final start =
|
|
|
|
+ first.getSelectionInRange(panStartOffset, panEndOffset).start;
|
|
|
|
+ final end = last.getSelectionInRange(panStartOffset, panEndOffset).end;
|
|
|
|
+ final selection = Selection(start: start, end: end);
|
|
debugPrint('[_onPanUpdate] isDownward = $isDownward, $selection');
|
|
debugPrint('[_onPanUpdate] isDownward = $isDownward, $selection');
|
|
- editorState.updateCursorSelection(selection);
|
|
|
|
-
|
|
|
|
- _scrollUpOrDownIfNeeded(panEndOffset!, isDownward);
|
|
|
|
|
|
+ updateSelection(selection);
|
|
}
|
|
}
|
|
|
|
|
|
- _showDebugLayerIfNeeded();
|
|
|
|
|
|
+ _showDebugLayerIfNeeded(offset: panEndOffset);
|
|
}
|
|
}
|
|
|
|
|
|
void _onPanEnd(DragEndDetails details) {
|
|
void _onPanEnd(DragEndDetails details) {
|
|
// do nothing
|
|
// do nothing
|
|
}
|
|
}
|
|
|
|
|
|
- void _updateSelection(Selection selection) {
|
|
|
|
- final nodes = _selectedNodesInSelection(editorState.document, selection);
|
|
|
|
|
|
+ void _updateSelectionAreas(Selection selection) {
|
|
|
|
+ final nodes = getNodesInSelection(selection);
|
|
|
|
|
|
currentSelectedNodes = nodes;
|
|
currentSelectedNodes = nodes;
|
|
- currentSelection.value = selection;
|
|
|
|
|
|
|
|
|
|
+ // TODO: need to be refactored.
|
|
Rect? topmostRect;
|
|
Rect? topmostRect;
|
|
LayerLink? layerLink;
|
|
LayerLink? layerLink;
|
|
|
|
|
|
- var index = 0;
|
|
|
|
- for (final node in nodes) {
|
|
|
|
|
|
+ final backwardNodes =
|
|
|
|
+ selection.isBackward ? nodes : nodes.reversed.toList(growable: false);
|
|
|
|
+ final backwardSelection = selection.isBackward
|
|
|
|
+ ? selection
|
|
|
|
+ : selection.copyWith(start: selection.end, end: selection.start);
|
|
|
|
+ assert(backwardSelection.isBackward);
|
|
|
|
+
|
|
|
|
+ for (var i = 0; i < backwardNodes.length; i++) {
|
|
|
|
+ final node = backwardNodes[i];
|
|
final selectable = node.selectable;
|
|
final selectable = node.selectable;
|
|
if (selectable == null) {
|
|
if (selectable == null) {
|
|
continue;
|
|
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());
|
|
|
|
- }
|
|
|
|
|
|
+ var newSelection = backwardSelection.copy();
|
|
|
|
+
|
|
|
|
+ /// In the case of multiple selections,
|
|
|
|
+ /// we need to return a new selection for each selected node individually.
|
|
|
|
+ ///
|
|
|
|
+ /// < > means selected.
|
|
|
|
+ /// text: abcd<ef
|
|
|
|
+ /// text: ghijkl
|
|
|
|
+ /// text: mn>opqr
|
|
|
|
+ ///
|
|
|
|
+ if (!backwardSelection.isSingle) {
|
|
|
|
+ if (i == 0) {
|
|
|
|
+ newSelection = newSelection.copyWith(end: selectable.end());
|
|
|
|
+ } else if (i == nodes.length - 1) {
|
|
|
|
+ newSelection = newSelection.copyWith(start: selectable.start());
|
|
} else {
|
|
} else {
|
|
- newSelection = selection.copyWith(
|
|
|
|
|
|
+ newSelection = Selection(
|
|
start: selectable.start(),
|
|
start: selectable.start(),
|
|
end: selectable.end(),
|
|
end: selectable.end(),
|
|
);
|
|
);
|
|
@@ -415,13 +375,13 @@ class _FlowySelectionState extends State<FlowySelection>
|
|
}
|
|
}
|
|
|
|
|
|
final rects = selectable.getRectsInSelection(newSelection);
|
|
final rects = selectable.getRectsInSelection(newSelection);
|
|
-
|
|
|
|
for (final rect in rects) {
|
|
for (final rect in rects) {
|
|
- // FIXME: Need to compute more precise location.
|
|
|
|
|
|
+ // TODO: Need to compute more precise location.
|
|
topmostRect ??= rect;
|
|
topmostRect ??= rect;
|
|
layerLink ??= node.layerLink;
|
|
layerLink ??= node.layerLink;
|
|
|
|
|
|
- _rects.add(_transformRectToGlobal(selectable, rect));
|
|
|
|
|
|
+ selectionRects.add(_transformRectToGlobal(selectable, rect));
|
|
|
|
+
|
|
final overlay = OverlayEntry(
|
|
final overlay = OverlayEntry(
|
|
builder: (context) => SelectionWidget(
|
|
builder: (context) => SelectionWidget(
|
|
color: widget.selectionColor,
|
|
color: widget.selectionColor,
|
|
@@ -429,11 +389,11 @@ class _FlowySelectionState extends State<FlowySelection>
|
|
rect: rect,
|
|
rect: rect,
|
|
),
|
|
),
|
|
);
|
|
);
|
|
- _selectionOverlays.add(overlay);
|
|
|
|
|
|
+ _selectionAreas.add(overlay);
|
|
}
|
|
}
|
|
- index += 1;
|
|
|
|
}
|
|
}
|
|
- Overlay.of(context)?.insertAll(_selectionOverlays);
|
|
|
|
|
|
+
|
|
|
|
+ Overlay.of(context)?.insertAll(_selectionAreas);
|
|
|
|
|
|
if (topmostRect != null && layerLink != null) {
|
|
if (topmostRect != null && layerLink != null) {
|
|
editorState.service.toolbarService
|
|
editorState.service.toolbarService
|
|
@@ -441,123 +401,84 @@ class _FlowySelectionState extends State<FlowySelection>
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
- Rect _transformRectToGlobal(Selectable selectable, Rect r) {
|
|
|
|
- final Offset topLeft = selectable.localToGlobal(Offset(r.left, r.top));
|
|
|
|
- return Rect.fromLTWH(topLeft.dx, topLeft.dy, r.width, r.height);
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- void _updateCursor(Position position) {
|
|
|
|
|
|
+ void _updateCursorAreas(Position position) {
|
|
final node = editorState.document.root.childAtPath(position.path);
|
|
final node = editorState.document.root.childAtPath(position.path);
|
|
|
|
|
|
- assert(node != null);
|
|
|
|
if (node == null) {
|
|
if (node == null) {
|
|
|
|
+ assert(false);
|
|
return;
|
|
return;
|
|
}
|
|
}
|
|
|
|
|
|
currentSelectedNodes = [node];
|
|
currentSelectedNodes = [node];
|
|
- currentSelection.value = Selection.collapsed(position);
|
|
|
|
|
|
|
|
|
|
+ _showCursor(node, position);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ void _showCursor(Node node, Position position) {
|
|
final selectable = node.selectable;
|
|
final selectable = node.selectable;
|
|
- final rect = selectable?.getCursorRectInPosition(position);
|
|
|
|
- if (rect != null) {
|
|
|
|
- _rects.add(_transformRectToGlobal(selectable!, rect));
|
|
|
|
- final cursor = OverlayEntry(
|
|
|
|
|
|
+ final cursorRect = selectable?.getCursorRectInPosition(position);
|
|
|
|
+ if (selectable != null && cursorRect != null) {
|
|
|
|
+ final cursorArea = OverlayEntry(
|
|
builder: (context) => CursorWidget(
|
|
builder: (context) => CursorWidget(
|
|
key: _cursorKey,
|
|
key: _cursorKey,
|
|
- rect: rect,
|
|
|
|
|
|
+ rect: cursorRect,
|
|
color: widget.cursorColor,
|
|
color: widget.cursorColor,
|
|
layerLink: node.layerLink,
|
|
layerLink: node.layerLink,
|
|
),
|
|
),
|
|
);
|
|
);
|
|
- _cursorOverlays.add(cursor);
|
|
|
|
- Overlay.of(context)?.insertAll(_cursorOverlays);
|
|
|
|
|
|
+
|
|
|
|
+ _cursorAreas.add(cursorArea);
|
|
|
|
+ selectionRects.add(_transformRectToGlobal(selectable, cursorRect));
|
|
|
|
+ Overlay.of(context)?.insertAll(_cursorAreas);
|
|
|
|
+
|
|
_forceShowCursor();
|
|
_forceShowCursor();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
- _forceShowCursor() {
|
|
|
|
- final currentState = _cursorKey.currentState as CursorWidgetState?;
|
|
|
|
- currentState?.show();
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- List<Node> _selectedNodesInSelection(
|
|
|
|
- StateTree stateTree, Selection selection) {
|
|
|
|
- final startNode = stateTree.nodeAtPath(selection.start.path)!;
|
|
|
|
- final endNode = stateTree.nodeAtPath(selection.end.path)!;
|
|
|
|
- return NodeIterator(stateTree, startNode, endNode).toList();
|
|
|
|
|
|
+ void _forceShowCursor() {
|
|
|
|
+ _cursorKey.currentState?.unwrapOrNull<CursorWidgetState>()?.show();
|
|
}
|
|
}
|
|
|
|
|
|
- void _scrollUpOrDownIfNeeded(Offset offset, bool isDownward) {
|
|
|
|
|
|
+ void _scrollUpOrDownIfNeeded() {
|
|
final dy = editorState.service.scrollService?.dy;
|
|
final dy = editorState.service.scrollService?.dy;
|
|
- if (dy == null) {
|
|
|
|
- assert(false, 'Dy could not be null');
|
|
|
|
|
|
+ final selectNodes = currentSelectedNodes;
|
|
|
|
+ final selection = currentSelection.value;
|
|
|
|
+ if (dy == null || selection == null || selectNodes.isEmpty) {
|
|
return;
|
|
return;
|
|
}
|
|
}
|
|
- final topLimit = MediaQuery.of(context).size.height * 0.2;
|
|
|
|
- final bottomLimit = MediaQuery.of(context).size.height * 0.8;
|
|
|
|
|
|
+
|
|
|
|
+ final rect = selectNodes.last.rect;
|
|
|
|
+
|
|
|
|
+ final size = MediaQuery.of(context).size.height;
|
|
|
|
+ final topLimit = size * 0.3;
|
|
|
|
+ final bottomLimit = size * 0.8;
|
|
|
|
|
|
/// TODO: It is necessary to calculate the relative speed
|
|
/// TODO: It is necessary to calculate the relative speed
|
|
/// according to the gap and move forward more gently.
|
|
/// according to the gap and move forward more gently.
|
|
- final distance = 10.0;
|
|
|
|
- if (offset.dy <= topLimit && !isDownward) {
|
|
|
|
- // up
|
|
|
|
- editorState.service.scrollService?.scrollTo(dy - distance);
|
|
|
|
- } else if (offset.dy >= bottomLimit && isDownward) {
|
|
|
|
- //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;
|
|
|
|
|
|
+ if (rect.top >= bottomLimit) {
|
|
|
|
+ if (selection.isSingle) {
|
|
|
|
+ editorState.service.scrollService?.scrollTo(dy + size * 0.2);
|
|
|
|
+ } else if (selection.isBackward) {
|
|
|
|
+ editorState.service.scrollService?.scrollTo(dy + 10.0);
|
|
|
|
+ }
|
|
|
|
+ } else if (rect.bottom <= topLimit) {
|
|
|
|
+ if (selection.isForward) {
|
|
|
|
+ editorState.service.scrollService?.scrollTo(dy - 10.0);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
- Node _lowerBoundInDocument(Offset offset) {
|
|
|
|
- final sortedNodes =
|
|
|
|
- editorState.document.root.children.toList(growable: false);
|
|
|
|
- return _lowerBound(sortedNodes, offset, 0, sortedNodes.length - 1);
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- Node _upperBoundInDocument(Offset offset) {
|
|
|
|
- final sortedNodes =
|
|
|
|
- editorState.document.root.children.toList(growable: false);
|
|
|
|
- return _upperBound(sortedNodes, offset, 0, sortedNodes.length - 1);
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- /// TODO: Supports multi-level nesting,
|
|
|
|
- /// currently only single-level nesting is supported
|
|
|
|
- // find the first node's rect.bottom <= offset.dy
|
|
|
|
- Node _lowerBound(List<Node> sortedNodes, Offset offset, int start, int end) {
|
|
|
|
- assert(start >= 0 && end < sortedNodes.length);
|
|
|
|
|
|
+ Node? _getNodeInOffset(
|
|
|
|
+ List<Node> sortedNodes, Offset offset, int start, int end) {
|
|
|
|
+ if (start < 0 && end >= sortedNodes.length) {
|
|
|
|
+ return null;
|
|
|
|
+ }
|
|
var min = start;
|
|
var min = start;
|
|
var max = end;
|
|
var max = end;
|
|
while (min <= max) {
|
|
while (min <= max) {
|
|
final mid = min + ((max - min) >> 1);
|
|
final mid = min + ((max - min) >> 1);
|
|
- if (sortedNodes[mid].rect.bottom <= offset.dy) {
|
|
|
|
|
|
+ final rect = sortedNodes[mid].rect;
|
|
|
|
+ if (rect.bottom <= offset.dy) {
|
|
min = mid + 1;
|
|
min = mid + 1;
|
|
} else {
|
|
} else {
|
|
max = mid - 1;
|
|
max = mid - 1;
|
|
@@ -566,144 +487,64 @@ class _FlowySelectionState extends State<FlowySelection>
|
|
final node = sortedNodes[min];
|
|
final node = sortedNodes[min];
|
|
if (node.children.isNotEmpty && node.children.first.rect.top <= offset.dy) {
|
|
if (node.children.isNotEmpty && node.children.first.rect.top <= offset.dy) {
|
|
final children = node.children.toList(growable: false);
|
|
final children = node.children.toList(growable: false);
|
|
- return _lowerBound(children, offset, 0, children.length - 1);
|
|
|
|
|
|
+ return _getNodeInOffset(
|
|
|
|
+ children,
|
|
|
|
+ offset,
|
|
|
|
+ 0,
|
|
|
|
+ children.length - 1,
|
|
|
|
+ );
|
|
}
|
|
}
|
|
return node;
|
|
return node;
|
|
}
|
|
}
|
|
|
|
|
|
- /// TODO: Supports multi-level nesting,
|
|
|
|
- /// currently only single-level nesting is supported
|
|
|
|
- // find the first node's rect.top < offset.dy
|
|
|
|
- Node _upperBound(
|
|
|
|
- List<Node> sortedNodes,
|
|
|
|
- Offset offset,
|
|
|
|
- int start,
|
|
|
|
- int end,
|
|
|
|
- ) {
|
|
|
|
- assert(start >= 0 && end < sortedNodes.length);
|
|
|
|
- var min = start;
|
|
|
|
- var max = end;
|
|
|
|
- while (min <= max) {
|
|
|
|
- final mid = min + ((max - min) >> 1);
|
|
|
|
- if (sortedNodes[mid].rect.top < offset.dy) {
|
|
|
|
- min = mid + 1;
|
|
|
|
- } else {
|
|
|
|
- max = mid - 1;
|
|
|
|
- }
|
|
|
|
- }
|
|
|
|
- final node = sortedNodes[max];
|
|
|
|
- if (node.children.isNotEmpty && node.children.first.rect.top <= offset.dy) {
|
|
|
|
- final children = node.children.toList(growable: false);
|
|
|
|
- return _lowerBound(children, offset, 0, children.length - 1);
|
|
|
|
- }
|
|
|
|
- return node;
|
|
|
|
|
|
+ void _enableInteraction() {
|
|
|
|
+ editorState.service.keyboardService?.enable();
|
|
|
|
+ editorState.service.scrollService?.enable();
|
|
}
|
|
}
|
|
-}
|
|
|
|
-
|
|
|
|
-/// 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.onTripleTapDown,
|
|
|
|
- this.onPanStart,
|
|
|
|
- this.onPanUpdate,
|
|
|
|
- this.onPanEnd})
|
|
|
|
- : super(key: key);
|
|
|
|
|
|
|
|
- @override
|
|
|
|
- State<_SelectionGestureDetector> createState() =>
|
|
|
|
- _SelectionGestureDetectorState();
|
|
|
|
-
|
|
|
|
- final Widget? child;
|
|
|
|
-
|
|
|
|
- final GestureTapDownCallback? onTapDown;
|
|
|
|
- final GestureTapDownCallback? onDoubleTapDown;
|
|
|
|
- final GestureTapDownCallback? onTripleTapDown;
|
|
|
|
- final GestureDragStartCallback? onPanStart;
|
|
|
|
- final GestureDragUpdateCallback? onPanUpdate;
|
|
|
|
- final GestureDragEndCallback? onPanEnd;
|
|
|
|
-}
|
|
|
|
-
|
|
|
|
-const Duration kTripleTapTimeout = Duration(milliseconds: 500);
|
|
|
|
|
|
+ Rect _transformRectToGlobal(Selectable selectable, Rect r) {
|
|
|
|
+ final Offset topLeft = selectable.localToGlobal(Offset(r.left, r.top));
|
|
|
|
+ return Rect.fromLTWH(topLeft.dx, topLeft.dy, r.width, r.height);
|
|
|
|
+ }
|
|
|
|
|
|
-class _SelectionGestureDetectorState extends State<_SelectionGestureDetector> {
|
|
|
|
- bool _isDoubleTap = false;
|
|
|
|
- Timer? _doubleTapTimer;
|
|
|
|
- int _tripleTabCount = 0;
|
|
|
|
- Timer? _tripleTabTimer;
|
|
|
|
- @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,
|
|
|
|
- );
|
|
|
|
|
|
+ void _onSelectionChange() {
|
|
|
|
+ _scrollUpOrDownIfNeeded();
|
|
}
|
|
}
|
|
|
|
|
|
- _tapDownDelegate(TapDownDetails tapDownDetails) {
|
|
|
|
- if (_tripleTabCount == 2) {
|
|
|
|
- _tripleTabCount = 0;
|
|
|
|
- _tripleTabTimer?.cancel();
|
|
|
|
- _tripleTabTimer = null;
|
|
|
|
- if (widget.onTripleTapDown != null) {
|
|
|
|
- widget.onTripleTapDown!(tapDownDetails);
|
|
|
|
- }
|
|
|
|
- } else if (_isDoubleTap) {
|
|
|
|
- _isDoubleTap = false;
|
|
|
|
- _doubleTapTimer?.cancel();
|
|
|
|
- _doubleTapTimer = null;
|
|
|
|
- if (widget.onDoubleTapDown != null) {
|
|
|
|
- widget.onDoubleTapDown!(tapDownDetails);
|
|
|
|
- }
|
|
|
|
- _tripleTabCount++;
|
|
|
|
- } else {
|
|
|
|
- if (widget.onTapDown != null) {
|
|
|
|
- widget.onTapDown!(tapDownDetails);
|
|
|
|
|
|
+ void _showDebugLayerIfNeeded({Offset? offset}) {
|
|
|
|
+ // remove false to show debug overlay.
|
|
|
|
+ if (kDebugMode && false) {
|
|
|
|
+ _debugOverlay?.remove();
|
|
|
|
+ if (offset != null) {
|
|
|
|
+ _debugOverlay = OverlayEntry(
|
|
|
|
+ builder: (context) => Positioned.fromRect(
|
|
|
|
+ rect: Rect.fromPoints(offset, offset.translate(20, 20)),
|
|
|
|
+ child: Container(
|
|
|
|
+ color: Colors.red.withOpacity(0.2),
|
|
|
|
+ ),
|
|
|
|
+ ),
|
|
|
|
+ );
|
|
|
|
+ Overlay.of(context)?.insert(_debugOverlay!);
|
|
|
|
+ } else if (_panStartOffset != null) {
|
|
|
|
+ _debugOverlay = OverlayEntry(
|
|
|
|
+ builder: (context) => Positioned.fromRect(
|
|
|
|
+ rect: Rect.fromPoints(
|
|
|
|
+ _panStartOffset?.translate(
|
|
|
|
+ 0,
|
|
|
|
+ -(editorState.service.scrollService!.dy -
|
|
|
|
+ _panStartScrollDy!),
|
|
|
|
+ ) ??
|
|
|
|
+ Offset.zero,
|
|
|
|
+ offset ?? Offset.zero),
|
|
|
|
+ child: Container(
|
|
|
|
+ color: Colors.red.withOpacity(0.2),
|
|
|
|
+ ),
|
|
|
|
+ ),
|
|
|
|
+ );
|
|
|
|
+ Overlay.of(context)?.insert(_debugOverlay!);
|
|
|
|
+ } else {
|
|
|
|
+ _debugOverlay = null;
|
|
}
|
|
}
|
|
-
|
|
|
|
- _isDoubleTap = true;
|
|
|
|
- _doubleTapTimer?.cancel();
|
|
|
|
- _doubleTapTimer = Timer(kDoubleTapTimeout, () {
|
|
|
|
- _isDoubleTap = false;
|
|
|
|
- _doubleTapTimer = null;
|
|
|
|
- });
|
|
|
|
-
|
|
|
|
- _tripleTabCount = 1;
|
|
|
|
- _tripleTabTimer?.cancel();
|
|
|
|
- _tripleTabTimer = Timer(kTripleTapTimeout, () {
|
|
|
|
- _tripleTabCount = 0;
|
|
|
|
- _tripleTabTimer = null;
|
|
|
|
- });
|
|
|
|
}
|
|
}
|
|
}
|
|
}
|
|
-
|
|
|
|
- @override
|
|
|
|
- void dispose() {
|
|
|
|
- _doubleTapTimer?.cancel();
|
|
|
|
- _tripleTabTimer?.cancel();
|
|
|
|
- super.dispose();
|
|
|
|
- }
|
|
|
|
}
|
|
}
|