diff --git a/lib/src/editor/block_component/base_component/selection/block_selection_area.dart b/lib/src/editor/block_component/base_component/selection/block_selection_area.dart index 16b4c623d..7b724c5b3 100644 --- a/lib/src/editor/block_component/base_component/selection/block_selection_area.dart +++ b/lib/src/editor/block_component/base_component/selection/block_selection_area.dart @@ -13,6 +13,7 @@ enum BlockSelectionType { cursor, selection, block, + dragAndDrop, } /// [BlockSelectionArea] is a widget that renders the selection area or the cursor of a block. @@ -22,12 +23,14 @@ class BlockSelectionArea extends StatefulWidget { required this.node, required this.delegate, required this.listenable, + this.dragAndDropListenable, required this.cursorColor, required this.selectionColor, required this.blockColor, this.supportTypes = const [ BlockSelectionType.cursor, BlockSelectionType.selection, + BlockSelectionType.dragAndDrop, ], }); @@ -37,6 +40,10 @@ class BlockSelectionArea extends StatefulWidget { // get the selection from the listenable final ValueListenable listenable; + // obtain the selection from dragAndDropListenable + // if it's `null`, construct the cursor for the drag-and-drop pointer + final ValueListenable? dragAndDropListenable; + // the color of the cursor final Color cursorColor; @@ -60,6 +67,9 @@ class _BlockSelectionAreaState extends State { debugLabel: 'cursor_${widget.node.path}', ); + // keep the previous drag and drop selection rects + // to avoid unnecessary rebuild + List? prevDragAndDropSelectionRects; // keep the previous cursor rect to avoid unnecessary rebuild Rect? prevCursorRect; // keep the previous selection rects to avoid unnecessary rebuild @@ -91,6 +101,24 @@ class _BlockSelectionAreaState extends State { valueListenable: widget.listenable, builder: ((context, value, child) { final sizedBox = child ?? const SizedBox.shrink(); + + final dragAndDropSelection = + context.read().dragAndDropSelection; + if (dragAndDropSelection != null && + widget.dragAndDropListenable != null) { + if (!widget.supportTypes.contains(BlockSelectionType.dragAndDrop) || + prevDragAndDropSelectionRects == null || + prevDragAndDropSelectionRects!.isEmpty || + (prevDragAndDropSelectionRects!.length == 1 && + prevDragAndDropSelectionRects!.first.width == 0)) { + return sizedBox; + } + return SelectionAreaPaint( + rects: prevDragAndDropSelectionRects!, + selectionColor: widget.selectionColor, + ); + } + final selection = value?.normalized; if (selection == null) { @@ -162,12 +190,29 @@ class _BlockSelectionAreaState extends State { if (!mounted) { return; } - final selection = widget.listenable.value?.normalized; final path = widget.node.path; + Selection? dragAndDropSelection; + if (widget.dragAndDropListenable != null) { + dragAndDropSelection = widget.dragAndDropListenable!.value?.normalized; + } + + if (dragAndDropSelection != null) { + if (widget.supportTypes.contains(BlockSelectionType.dragAndDrop)) { + final rects = widget.delegate.getRectsInSelection(dragAndDropSelection); + if (!_deepEqual(rects, prevSelectionRects)) { + setState(() { + prevDragAndDropSelectionRects = rects; + prevSelectionRects = null; + prevCursorRect = null; + prevBlockRect = null; + }); + } + } + } // the current path is in the selection - if (selection != null && path.inSelection(selection)) { + else if (selection != null && path.inSelection(selection)) { if (widget.supportTypes.contains(BlockSelectionType.block) && context.read().selectionType == SelectionType.block) { if (!path.equals(selection.start.path)) { diff --git a/lib/src/editor/block_component/base_component/selection/block_selection_container.dart b/lib/src/editor/block_component/base_component/selection/block_selection_container.dart index 9ceb3882f..32fb292e4 100644 --- a/lib/src/editor/block_component/base_component/selection/block_selection_container.dart +++ b/lib/src/editor/block_component/base_component/selection/block_selection_container.dart @@ -8,12 +8,14 @@ class BlockSelectionContainer extends StatelessWidget { required this.node, required this.delegate, required this.listenable, + required this.dragAndDropListenable, this.cursorColor = Colors.black, this.selectionColor = Colors.blue, this.blockColor = Colors.blue, this.supportTypes = const [ BlockSelectionType.cursor, BlockSelectionType.selection, + BlockSelectionType.dragAndDrop, ], required this.child, }); @@ -24,6 +26,8 @@ class BlockSelectionContainer extends StatelessWidget { // get the selection from the listenable final ValueListenable listenable; + final ValueListenable dragAndDropListenable; + // the color of the cursor final Color cursorColor; @@ -55,6 +59,7 @@ class BlockSelectionContainer extends StatelessWidget { node: node, delegate: delegate, listenable: listenable, + dragAndDropListenable: dragAndDropListenable, cursorColor: cursorColor, selectionColor: selectionColor, blockColor: blockColor, diff --git a/lib/src/editor/block_component/bulleted_list_block_component/bulleted_list_block_component.dart b/lib/src/editor/block_component/bulleted_list_block_component/bulleted_list_block_component.dart index 4d8415a6f..b2d848b46 100644 --- a/lib/src/editor/block_component/bulleted_list_block_component/bulleted_list_block_component.dart +++ b/lib/src/editor/block_component/bulleted_list_block_component/bulleted_list_block_component.dart @@ -106,6 +106,42 @@ class _BulletedListBlockComponentWidgetState @override Node get node => widget.node; + CursorStyle _cursorStyle = CursorStyle.verticalLine; + + @override + CursorStyle get cursorStyle => _cursorStyle; + + set cursorStyle(CursorStyle cursorStyle) { + _cursorStyle = cursorStyle; + } + + bool _shouldCursorBlink = true; + + @override + bool get shouldCursorBlink => _shouldCursorBlink; + + set shouldCursorBlink(bool value) { + _shouldCursorBlink = value; + } + + void _onCursorStyleChange() { + cursorStyle = editorState.cursorStyle; + + shouldCursorBlink = cursorStyle != CursorStyle.dottedVerticalLine; + } + + @override + void initState() { + super.initState(); + editorState.cursorStyleNotifier.addListener(_onCursorStyleChange); + } + + @override + void dispose() { + editorState.cursorStyleNotifier.removeListener(_onCursorStyleChange); + super.dispose(); + } + @override Widget buildComponent( BuildContext context, { @@ -168,6 +204,7 @@ class _BulletedListBlockComponentWidgetState node: node, delegate: this, listenable: editorState.selectionNotifier, + dragAndDropListenable: editorState.dragAndDropSelectionNotifier, blockColor: editorState.editorStyle.selectionColor, supportTypes: const [ BlockSelectionType.block, diff --git a/lib/src/editor/block_component/divider_block_component/divider_block_component.dart b/lib/src/editor/block_component/divider_block_component/divider_block_component.dart index 7805ff31f..912ac98a9 100644 --- a/lib/src/editor/block_component/divider_block_component/divider_block_component.dart +++ b/lib/src/editor/block_component/divider_block_component/divider_block_component.dart @@ -111,6 +111,7 @@ class _DividerBlockComponentWidgetState node: node, delegate: this, listenable: editorState.selectionNotifier, + dragAndDropListenable: editorState.dragAndDropSelectionNotifier, blockColor: editorState.editorStyle.selectionColor, cursorColor: editorState.editorStyle.cursorColor, selectionColor: editorState.editorStyle.selectionColor, diff --git a/lib/src/editor/block_component/heading_block_component/heading_block_component.dart b/lib/src/editor/block_component/heading_block_component/heading_block_component.dart index 5b0a71afc..dbab1feb7 100644 --- a/lib/src/editor/block_component/heading_block_component/heading_block_component.dart +++ b/lib/src/editor/block_component/heading_block_component/heading_block_component.dart @@ -119,6 +119,42 @@ class _HeadingBlockComponentWidgetState int get level => widget.node.attributes[HeadingBlockKeys.level] as int? ?? 1; + CursorStyle _cursorStyle = CursorStyle.verticalLine; + + @override + CursorStyle get cursorStyle => _cursorStyle; + + set cursorStyle(CursorStyle cursorStyle) { + _cursorStyle = cursorStyle; + } + + bool _shouldCursorBlink = true; + + @override + bool get shouldCursorBlink => _shouldCursorBlink; + + set shouldCursorBlink(bool value) { + _shouldCursorBlink = value; + } + + void _onCursorStyleChange() { + cursorStyle = editorState.cursorStyle; + + shouldCursorBlink = cursorStyle != CursorStyle.dottedVerticalLine; + } + + @override + void initState() { + super.initState(); + editorState.cursorStyleNotifier.addListener(_onCursorStyleChange); + } + + @override + void dispose() { + editorState.cursorStyleNotifier.removeListener(_onCursorStyleChange); + super.dispose(); + } + @override Widget build(BuildContext context) { final textDirection = calculateTextDirection( @@ -183,6 +219,7 @@ class _HeadingBlockComponentWidgetState node: node, delegate: this, listenable: editorState.selectionNotifier, + dragAndDropListenable: editorState.dragAndDropSelectionNotifier, blockColor: editorState.editorStyle.selectionColor, supportTypes: const [ BlockSelectionType.block, diff --git a/lib/src/editor/block_component/image_block_component/image_block_component.dart b/lib/src/editor/block_component/image_block_component/image_block_component.dart index 3da81ccb2..0727ea95a 100644 --- a/lib/src/editor/block_component/image_block_component/image_block_component.dart +++ b/lib/src/editor/block_component/image_block_component/image_block_component.dart @@ -162,6 +162,7 @@ class ImageBlockComponentWidgetState extends State node: node, delegate: this, listenable: editorState.selectionNotifier, + dragAndDropListenable: editorState.dragAndDropSelectionNotifier, blockColor: editorState.editorStyle.selectionColor, supportTypes: const [ BlockSelectionType.block, @@ -196,6 +197,8 @@ class ImageBlockComponentWidgetState extends State node: node, delegate: this, listenable: editorState.selectionNotifier, + dragAndDropListenable: + editorState.dragAndDropSelectionNotifier, cursorColor: editorState.editorStyle.cursorColor, selectionColor: editorState.editorStyle.selectionColor, child: child!, diff --git a/lib/src/editor/block_component/numbered_list_block_component/numbered_list_block_component.dart b/lib/src/editor/block_component/numbered_list_block_component/numbered_list_block_component.dart index 918eb3159..626de7069 100644 --- a/lib/src/editor/block_component/numbered_list_block_component/numbered_list_block_component.dart +++ b/lib/src/editor/block_component/numbered_list_block_component/numbered_list_block_component.dart @@ -112,6 +112,42 @@ class _NumberedListBlockComponentWidgetState @override Node get node => widget.node; + CursorStyle _cursorStyle = CursorStyle.verticalLine; + + @override + CursorStyle get cursorStyle => _cursorStyle; + + set cursorStyle(CursorStyle cursorStyle) { + _cursorStyle = cursorStyle; + } + + bool _shouldCursorBlink = true; + + @override + bool get shouldCursorBlink => _shouldCursorBlink; + + set shouldCursorBlink(bool value) { + _shouldCursorBlink = value; + } + + void _onCursorStyleChange() { + cursorStyle = editorState.cursorStyle; + + shouldCursorBlink = cursorStyle != CursorStyle.dottedVerticalLine; + } + + @override + void initState() { + super.initState(); + editorState.cursorStyleNotifier.addListener(_onCursorStyleChange); + } + + @override + void dispose() { + editorState.cursorStyleNotifier.removeListener(_onCursorStyleChange); + super.dispose(); + } + @override Widget buildComponent( BuildContext context, { @@ -175,6 +211,7 @@ class _NumberedListBlockComponentWidgetState node: node, delegate: this, listenable: editorState.selectionNotifier, + dragAndDropListenable: editorState.dragAndDropSelectionNotifier, blockColor: editorState.editorStyle.selectionColor, supportTypes: const [ BlockSelectionType.block, diff --git a/lib/src/editor/block_component/paragraph_block_component/paragraph_block_component.dart b/lib/src/editor/block_component/paragraph_block_component/paragraph_block_component.dart index 46cc8d45e..6f5d89aca 100644 --- a/lib/src/editor/block_component/paragraph_block_component/paragraph_block_component.dart +++ b/lib/src/editor/block_component/paragraph_block_component/paragraph_block_component.dart @@ -111,16 +111,36 @@ class _ParagraphBlockComponentWidgetState bool _showPlaceholder = false; + CursorStyle _cursorStyle = CursorStyle.verticalLine; + + @override + CursorStyle get cursorStyle => _cursorStyle; + + set cursorStyle(CursorStyle cursorStyle) { + _cursorStyle = cursorStyle; + } + + bool _shouldCursorBlink = true; + + @override + bool get shouldCursorBlink => _shouldCursorBlink; + + set shouldCursorBlink(bool value) { + _shouldCursorBlink = value; + } + @override void initState() { super.initState(); editorState.selectionNotifier.addListener(_onSelectionChange); + editorState.cursorStyleNotifier.addListener(_onCursorStyleChange); _onSelectionChange(); } @override void dispose() { editorState.selectionNotifier.removeListener(_onSelectionChange); + editorState.cursorStyleNotifier.removeListener(_onCursorStyleChange); super.dispose(); } @@ -140,6 +160,12 @@ class _ParagraphBlockComponentWidgetState } } + void _onCursorStyleChange() { + cursorStyle = editorState.cursorStyle; + + shouldCursorBlink = cursorStyle != CursorStyle.dottedVerticalLine; + } + @override Widget buildComponent( BuildContext context, { @@ -191,6 +217,7 @@ class _ParagraphBlockComponentWidgetState node: node, delegate: this, listenable: editorState.selectionNotifier, + dragAndDropListenable: editorState.dragAndDropSelectionNotifier, blockColor: editorState.editorStyle.selectionColor, supportTypes: const [ BlockSelectionType.block, diff --git a/lib/src/editor/block_component/quote_block_component/quote_block_component.dart b/lib/src/editor/block_component/quote_block_component/quote_block_component.dart index d71b5dc85..55f8bd653 100644 --- a/lib/src/editor/block_component/quote_block_component/quote_block_component.dart +++ b/lib/src/editor/block_component/quote_block_component/quote_block_component.dart @@ -105,6 +105,42 @@ class _QuoteBlockComponentWidgetState extends State @override late final editorState = Provider.of(context, listen: false); + CursorStyle _cursorStyle = CursorStyle.verticalLine; + + @override + CursorStyle get cursorStyle => _cursorStyle; + + set cursorStyle(CursorStyle cursorStyle) { + _cursorStyle = cursorStyle; + } + + bool _shouldCursorBlink = true; + + @override + bool get shouldCursorBlink => _shouldCursorBlink; + + set shouldCursorBlink(bool value) { + _shouldCursorBlink = value; + } + + void _onCursorStyleChange() { + cursorStyle = editorState.cursorStyle; + + shouldCursorBlink = cursorStyle != CursorStyle.dottedVerticalLine; + } + + @override + void initState() { + super.initState(); + editorState.cursorStyleNotifier.addListener(_onCursorStyleChange); + } + + @override + void dispose() { + editorState.cursorStyleNotifier.removeListener(_onCursorStyleChange); + super.dispose(); + } + @override Widget build(BuildContext context) { final textDirection = calculateTextDirection( @@ -163,6 +199,7 @@ class _QuoteBlockComponentWidgetState extends State node: node, delegate: this, listenable: editorState.selectionNotifier, + dragAndDropListenable: editorState.dragAndDropSelectionNotifier, blockColor: editorState.editorStyle.selectionColor, supportTypes: const [ BlockSelectionType.block, diff --git a/lib/src/editor/block_component/rich_text/appflowy_rich_text.dart b/lib/src/editor/block_component/rich_text/appflowy_rich_text.dart index b023df8f9..11394c3d1 100644 --- a/lib/src/editor/block_component/rich_text/appflowy_rich_text.dart +++ b/lib/src/editor/block_component/rich_text/appflowy_rich_text.dart @@ -108,21 +108,27 @@ class _AppFlowyRichTextState extends State @override Widget build(BuildContext context) { - final child = MouseRegion( - cursor: SystemMouseCursors.text, - child: widget.node.delta?.toPlainText().isEmpty ?? true - ? Stack( - children: [ - _buildPlaceholderText(context), - _buildRichText(context), - ], - ) - : _buildRichText(context), + final child = ValueListenableBuilder( + valueListenable: widget.editorState.mouseCursorStyleNotifier, + builder: (context, value, child) { + return MouseRegion( + cursor: value, + child: widget.node.delta?.toPlainText().isEmpty ?? true + ? Stack( + children: [ + _buildPlaceholderText(context), + _buildRichText(context), + ], + ) + : _buildRichText(context), + ); + }, ); return BlockSelectionContainer( delegate: widget.delegate, listenable: widget.editorState.selectionNotifier, + dragAndDropListenable: widget.editorState.dragAndDropSelectionNotifier, node: widget.node, cursorColor: widget.cursorColor, selectionColor: widget.selectionColor, diff --git a/lib/src/editor/block_component/table_block_component/table_block_component.dart b/lib/src/editor/block_component/table_block_component/table_block_component.dart index a703252e5..549488e33 100644 --- a/lib/src/editor/block_component/table_block_component/table_block_component.dart +++ b/lib/src/editor/block_component/table_block_component/table_block_component.dart @@ -165,6 +165,7 @@ class _TableBlockComponentWidgetState extends State node: node, delegate: this, listenable: editorState.selectionNotifier, + dragAndDropListenable: editorState.dragAndDropSelectionNotifier, blockColor: editorState.editorStyle.selectionColor, supportTypes: const [ BlockSelectionType.block, diff --git a/lib/src/editor/block_component/todo_list_block_component/todo_list_block_component.dart b/lib/src/editor/block_component/todo_list_block_component/todo_list_block_component.dart index e7408dcc5..d3a7f250b 100644 --- a/lib/src/editor/block_component/todo_list_block_component/todo_list_block_component.dart +++ b/lib/src/editor/block_component/todo_list_block_component/todo_list_block_component.dart @@ -129,6 +129,42 @@ class _TodoListBlockComponentWidgetState bool get checked => widget.node.attributes[TodoListBlockKeys.checked]; + CursorStyle _cursorStyle = CursorStyle.verticalLine; + + @override + CursorStyle get cursorStyle => _cursorStyle; + + set cursorStyle(CursorStyle cursorStyle) { + _cursorStyle = cursorStyle; + } + + bool _shouldCursorBlink = true; + + @override + bool get shouldCursorBlink => _shouldCursorBlink; + + set shouldCursorBlink(bool value) { + _shouldCursorBlink = value; + } + + void _onCursorStyleChange() { + cursorStyle = editorState.cursorStyle; + + shouldCursorBlink = cursorStyle != CursorStyle.dottedVerticalLine; + } + + @override + void initState() { + super.initState(); + editorState.cursorStyleNotifier.addListener(_onCursorStyleChange); + } + + @override + void dispose() { + editorState.cursorStyleNotifier.removeListener(_onCursorStyleChange); + super.dispose(); + } + @override Widget buildComponent( BuildContext context, { @@ -193,6 +229,7 @@ class _TodoListBlockComponentWidgetState node: node, delegate: this, listenable: editorState.selectionNotifier, + dragAndDropListenable: editorState.dragAndDropSelectionNotifier, blockColor: editorState.editorStyle.selectionColor, supportTypes: const [ BlockSelectionType.block, diff --git a/lib/src/editor/command/selection_commands.dart b/lib/src/editor/command/selection_commands.dart index 105a00836..e697fddc7 100644 --- a/lib/src/editor/command/selection_commands.dart +++ b/lib/src/editor/command/selection_commands.dart @@ -227,7 +227,7 @@ extension SelectionTransform on EditorState { } // Originally, I want to make this function as pure as possible, - // but I have to import the selectable here to compute the selection. + // but I have to import the selectable here to compute the selection. final start = node.selectable?.start(); final end = node.selectable?.end(); final offset = direction == SelectionMoveDirection.forward diff --git a/lib/src/editor/command/text_commands.dart b/lib/src/editor/command/text_commands.dart index d7d695939..92765c210 100644 --- a/lib/src/editor/command/text_commands.dart +++ b/lib/src/editor/command/text_commands.dart @@ -344,6 +344,11 @@ extension TextTransforms on EditorState { if (selection == null || selection.isCollapsed) { return res; } + + if (selection.isForward) { + selection = selection.reversed; + } + final nodes = getNodesInSelection(selection); for (final node in nodes) { final delta = node.delta; diff --git a/lib/src/editor/editor_component/service/selection/desktop_selection_service.dart b/lib/src/editor/editor_component/service/selection/desktop_selection_service.dart index 318024b2c..1f165f1ea 100644 --- a/lib/src/editor/editor_component/service/selection/desktop_selection_service.dart +++ b/lib/src/editor/editor_component/service/selection/desktop_selection_service.dart @@ -8,6 +8,9 @@ import 'package:flutter/material.dart' hide Overlay, OverlayEntry; import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; +const int negativeVerticalOffset = 12; +const int negativeHorizontalOffset = 10; + class DesktopSelectionServiceWidget extends StatefulWidget { const DesktopSelectionServiceWidget({ super.key, @@ -39,6 +42,7 @@ class _DesktopSelectionServiceWidgetState @override ValueNotifier currentSelection = ValueNotifier(null); + ValueNotifier currentDragAndDropSelection = ValueNotifier(null); @override List get currentSelectedNodes => editorState.getSelectedNodes(); @@ -51,6 +55,53 @@ class _DesktopSelectionServiceWidgetState Position? _panStartPosition; + // stores multiple selectable objects + // for supporting multi-line selection. + final Set> _dragAndDropSelectables = {}; + + /// stores the calculated rect for each selectable + /// object in `dragAndDropSelectables`. + final Set _dragAndDropSelectionRects = {}; + + /// `true`, if the cursor is inside the selected + /// rect on drag and drop operation. + bool _isCursorPointValid = false; + + // cursor position calculated during drag and drop op. + double cursorX = 0; + double cursorY = 0; + + List? _cachedDragAndDropSelectionRects; + + List get dragAndDropSelectionRects { + _cachedDragAndDropSelectionRects ??= + _dragAndDropSelectionRects.toList(growable: false); + return _cachedDragAndDropSelectionRects!; + } + + set dragAndDropSelectionRect(Rect rect) { + if (_dragAndDropSelectionRects.contains(rect)) return; + + _dragAndDropSelectionRects.add(rect); + _cachedDragAndDropSelectionRects = null; + } + + List>? _cachedDragAndDropSelectables; + + List> get dragAndDropSelectables { + _cachedDragAndDropSelectables ??= + _dragAndDropSelectables.toList(growable: false); + return _cachedDragAndDropSelectables!; + } + + set dragAndDropSelectable(SelectableMixin selectable) { + if (_dragAndDropSelectables.contains(selectable)) return; + + _dragAndDropSelectables.add(selectable); + _cachedDragAndDropSelectables = null; + _cachedDragAndDropSelectionRects = null; + } + late EditorState editorState = Provider.of( context, listen: false, @@ -94,6 +145,7 @@ class _DesktopSelectionServiceWidgetState onPanStart: _onPanStart, onPanUpdate: _onPanUpdate, onPanEnd: _onPanEnd, + onTapUp: _onTapUp, onTapDown: _onTapDown, onSecondaryTapDown: _onSecondaryTapDown, onDoubleTapDown: _onDoubleTapDown, @@ -111,6 +163,22 @@ class _DesktopSelectionServiceWidgetState ); } + void updateDragAndDropSelection(Selection? selection) { + currentDragAndDropSelection.value = selection; + editorState.updateDragAndDropSelectionWithReason( + selection, + reason: SelectionUpdateReason.uiEvent, + ); + } + + void updateMouseCursorStyle(SystemMouseCursor cursorStyle) { + editorState.updateMouseCursorStyle(cursorStyle); + } + + void updateCursorStyle(CursorStyle cursorStyle) { + editorState.updateCursorStyle(cursorStyle); + } + @override void clearSelection() { // currentSelectedNodes = []; @@ -216,6 +284,84 @@ class _DesktopSelectionServiceWidgetState throw UnimplementedError(); } + // Resets the presets for drag and drop selection after the operation + void reset() { + cursorX = cursorY = 0; + + _cachedDragAndDropSelectionRects = null; + _dragAndDropSelectionRects.clear(); + + _cachedDragAndDropSelectables = null; + _dragAndDropSelectables.clear(); + + clearSelection(); + updateCursorStyle(CursorStyle.verticalLine); + updateMouseCursorStyle(SystemMouseCursors.text); + updateDragAndDropSelection(null); + } + + /// Checks if the cursor position, specified by `dx` and `dy`, is within the + /// bounds of the given `rect`. + /// + /// Optionally, removes applied negative offsets to the boundaries based on the + /// `removeNegativeOffset` parameter. + /// + /// Returns `true` if the cursor is within the selection, and `false` otherwise. + bool isCursorInSelection( + double dx, + double dy, + Rect rect, { + bool removeNegativeOffset = false, + }) { + final int horizontalOffset = + removeNegativeOffset ? negativeHorizontalOffset : 0; + final int verticalOffset = + removeNegativeOffset ? negativeVerticalOffset : 0; + + if (dx < (rect.left - horizontalOffset) || + dy < (rect.top - verticalOffset) || + dx > (rect.right + horizontalOffset) || + dy > (rect.bottom + verticalOffset)) { + return false; + } + return true; + } + + /// Calculate the bounding box around a set of rectangles and adjust + /// the coordinates by subtracting a `negative offset`. This adjustment + /// eliminates boundaries around the cursor selection, facilitating + /// drag and drop of text content without obstruction. + Rect calculateRect(SelectableMixin selectable) { + final rects = selectable.getRectsInSelection( + currentDragAndDropSelection.value!, + ); + + double left = 0.0, top = 0.0, right = 0.0, bottom = 0.0; + for (final rect in rects) { + left = math.min(left, rect.left); + right = math.max(right, rect.right); + top = math.min(top, rect.top); + bottom = math.max(bottom, rect.bottom); + } + + final leftTopOffset = selectable.localToGlobal(Offset(left, top)); + final rightBottomOffset = selectable.localToGlobal(Offset(right, bottom)); + + // Added negative offset to eliminate rect boundaries + left = leftTopOffset.dx + negativeHorizontalOffset; + top = leftTopOffset.dy + negativeVerticalOffset; + right = rightBottomOffset.dx - negativeHorizontalOffset; + bottom = rightBottomOffset.dy - negativeVerticalOffset; + + return Rect.fromLTRB(left, top, right, bottom); + } + + void _onTapUp(TapUpDetails details) { + if (_isCursorPointValid) { + reset(); + } + } + void _onTapDown(TapDownDetails details) { _clearContextMenu(); @@ -223,12 +369,37 @@ class _DesktopSelectionServiceWidgetState (element) => element.canTap?.call(details) ?? true, ); if (!canTap) { + reset(); return updateSelection(null); } final offset = details.globalPosition; final node = getNodeInOffset(offset); final selectable = node?.selectable; + + if (currentDragAndDropSelection.value != null) { + if (_cachedDragAndDropSelectionRects == null) { + for (final selectable in dragAndDropSelectables) { + dragAndDropSelectionRect = calculateRect(selectable); + } + } + + cursorX = offset.dx; + cursorY = offset.dy; + + for (final rect in dragAndDropSelectionRects) { + if (isCursorInSelection( + cursorX, + cursorY, + rect, + removeNegativeOffset: true, + )) { + _isCursorPointValid = true; + return; + } + } + } + if (selectable == null) { // Clear old start offset _panStartOffset = null; @@ -256,16 +427,23 @@ class _DesktopSelectionServiceWidgetState } updateSelection(selection); + + // need to cancel drag and drop op. selection + // on single tap down event. + reset(); } void _onDoubleTapDown(TapDownDetails details) { final offset = details.globalPosition; final node = getNodeInOffset(offset); - final selection = node?.selectable?.getWordBoundaryInOffset(offset); + final selectable = node?.selectable; + final selection = selectable?.getWordBoundaryInOffset(offset); if (selection == null) { clearSelection(); return; } + dragAndDropSelectable = selectable!; + updateDragAndDropSelection(selection); updateSelection(selection); } @@ -281,6 +459,8 @@ class _DesktopSelectionServiceWidgetState start: selectable.start(), end: selectable.end(), ); + dragAndDropSelectable = selectable; + updateDragAndDropSelection(selection); updateSelection(selection); } @@ -316,8 +496,17 @@ class _DesktopSelectionServiceWidgetState return; } - final panEndOffset = details.globalPosition; final dy = editorState.service.scrollService?.dy; + + if (_isCursorPointValid) { + cursorX = details.globalPosition.dx; + cursorY = details.globalPosition.dy; + + updateCursorPosition(details.globalPosition); + return; + } + + final panEndOffset = details.globalPosition; final panStartOffset = dy == null ? _panStartOffset! : _panStartOffset!.translate(0, _panStartScrollDy! - dy); @@ -330,6 +519,8 @@ class _DesktopSelectionServiceWidgetState final start = _panStartPosition!; final end = last.getSelectionInRange(panStartOffset, panEndOffset).end; final selection = Selection(start: start, end: end); + dragAndDropSelectable = last; + updateDragAndDropSelection(selection); updateSelection(selection); } @@ -343,6 +534,20 @@ class _DesktopSelectionServiceWidgetState _panStartPosition = null; editorState.service.scrollService?.stopAutoScroll(); + + if (_isCursorPointValid) { + for (final rect in dragAndDropSelectionRects) { + if (!isCursorInSelection(cursorX, cursorY, rect)) { + moveSelection( + currentDragAndDropSelection.value, + currentSelection.value, + ); + break; + } + } + reset(); + _isCursorPointValid = false; + } } void _updateSelection() { @@ -465,6 +670,90 @@ class _DesktopSelectionServiceWidgetState return min.clamp(start, end); } + void updateCursorPosition(Offset offset) { + final selection = currentDragAndDropSelection.value; + if (selection == null) return; + + final node = getNodeInOffset(offset); + final selectable = node?.selectable; + if (selectable == null) { + reset(); + return; + } + + updateCursorStyle(CursorStyle.dottedVerticalLine); + updateMouseCursorStyle(SystemMouseCursors.basic); + updateSelection( + Selection.collapsed( + selectable.getPositionInOffset(offset), + ), + ); + } + + void moveSelection(Selection? selection, Selection? cursorPosition) { + if (selection == null || cursorPosition == null) return; + + final fromNode = editorState.getNodeAtPath(selection.start.path); + final toNode = editorState.getNodeAtPath(cursorPosition.start.path); + if (fromNode == null || toNode == null) return; + + final textInSelection = editorState.getTextInSelection(selection); + int len = 0; + + for (int i = textInSelection.length - 1; i >= 0; i--) { + String text = textInSelection[i]; + text += (textInSelection.length > 1 && i < textInSelection.length - 1) + ? ' ' + : ''; + len += text.length; + editorState.insertText(cursorPosition.start.offset, text, node: toNode); + } + + Selection newCursorPosition = cursorPosition; + + // Update the offset of the selection if: + // + // The selection is at the same node, and + // The drop cursor position is before the selection. + // + int end = selection.endIndex; + if (selection.isForward) { + end = selection.startIndex; + } + if (fromNode == toNode && cursorPosition.startIndex < end) { + final newStartPosition = Position( + path: fromNode.path, + offset: selection.start.offset + len, + ); + final newEndPosition = Position( + path: fromNode.path, + offset: selection.end.offset + len, + ); + + selection = Selection( + start: newStartPosition, + end: newEndPosition, + ); + } + + editorState.deleteSelection(selection); + + if (fromNode != toNode || + (fromNode == toNode && cursorPosition.startIndex < end)) { + newCursorPosition = Selection.collapsed( + Position( + path: toNode.path, + offset: cursorPosition.startIndex + len, + ), + ); + } + + // update the cursor position to the last node or + // the cursor point of the edited [toNode] path after + // the drag and drop operation + updateSelection(newCursorPosition); + } + @override void registerGestureInterceptor(SelectionGestureInterceptor interceptor) { _interceptors.add(interceptor); diff --git a/lib/src/editor_state.dart b/lib/src/editor_state.dart index 963560f21..103be0bf4 100644 --- a/lib/src/editor_state.dart +++ b/lib/src/editor_state.dart @@ -5,6 +5,7 @@ import 'package:appflowy_editor/src/editor/editor_component/service/scroll/auto_ import 'package:appflowy_editor/src/history/undo_manager.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart' hide UndoManager; /// the type of this value is bool. /// @@ -112,12 +113,52 @@ class EditorState { selectionNotifier.value = value; } + ValueNotifier cursorStyleNotifier = + ValueNotifier(CursorStyle.verticalLine); + + CursorStyle get cursorStyle => cursorStyleNotifier.value; + + set cursorStyle(CursorStyle cursorStyle) { + cursorStyleNotifier.value = cursorStyle; + } + + ValueNotifier mouseCursorStyleNotifier = + ValueNotifier(SystemMouseCursors.text); + + SystemMouseCursor get mouseCursorStyle => mouseCursorStyleNotifier.value; + + set mouseCursorStyle(SystemMouseCursor cursorStyle) { + mouseCursorStyleNotifier.value = cursorStyle; + } + + /// The drag and drop selection notifier of the editor. + final PropertyValueNotifier dragAndDropSelectionNotifier = + PropertyValueNotifier(null); + + /// The drag and drop selection of the editor. + Selection? get dragAndDropSelection => dragAndDropSelectionNotifier.value; + + /// Sets the drag and drop selection of the editor. + set dragAndDropSelection(Selection? value) { + // clear the toggled style when the selection is changed. + toggledStyle.clear(); + + dragAndDropSelectionNotifier.value = value; + } + SelectionType? selectionType; + SelectionType? dragAndDropSelectionType; SelectionUpdateReason _selectionUpdateReason = SelectionUpdateReason.uiEvent; SelectionUpdateReason get selectionUpdateReason => _selectionUpdateReason; + SelectionUpdateReason _dragAndDropSelectionUpdateReason = + SelectionUpdateReason.uiEvent; + SelectionUpdateReason get dragAndDropSelectionUpdateReason => + _dragAndDropSelectionUpdateReason; + Map? selectionExtraInfo; + Map? dragAndDropSelectionExtraInfo; // Service reference. final service = EditorService(); @@ -221,6 +262,63 @@ class EditorState { return completer.future; } + Future updateDragAndDropSelectionWithReason( + Selection? selection, { + SelectionUpdateReason reason = SelectionUpdateReason.transaction, + Map? extraInfo, + }) async { + final completer = Completer(); + + if (reason == SelectionUpdateReason.uiEvent) { + dragAndDropSelectionType = SelectionType.inline; + WidgetsBinding.instance.addPostFrameCallback( + (timeStamp) => completer.complete(), + ); + } + + // broadcast to other users here + dragAndDropSelectionExtraInfo = extraInfo; + _dragAndDropSelectionUpdateReason = reason; + + dragAndDropSelection = selection; + + return completer.future; + } + + Future updateMouseCursorStyle( + SystemMouseCursor cursorStyle, { + SelectionUpdateReason reason = SelectionUpdateReason.transaction, + }) async { + final completer = Completer(); + + if (reason == SelectionUpdateReason.uiEvent) { + WidgetsBinding.instance.addPostFrameCallback( + (timeStamp) => completer.complete(), + ); + } + + mouseCursorStyle = cursorStyle; + + return completer.future; + } + + Future updateCursorStyle( + CursorStyle cursorStyle, { + SelectionUpdateReason reason = SelectionUpdateReason.transaction, + }) async { + final completer = Completer(); + + if (reason == SelectionUpdateReason.uiEvent) { + WidgetsBinding.instance.addPostFrameCallback( + (timeStamp) => completer.complete(), + ); + } + + this.cursorStyle = cursorStyle; + + return completer.future; + } + @Deprecated('use updateSelectionWithReason or editorState.selection instead') Future updateCursorSelection( Selection? cursorSelection, [ diff --git a/lib/src/render/selection/cursor.dart b/lib/src/render/selection/cursor.dart index b7b0573ff..08810c52a 100644 --- a/lib/src/render/selection/cursor.dart +++ b/lib/src/render/selection/cursor.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:appflowy_editor/src/render/selection/dashed_cursor_painter.dart'; import 'package:appflowy_editor/src/render/selection/selectable.dart'; import 'package:flutter/material.dart'; @@ -76,6 +77,12 @@ class CursorState extends State { return Container( color: color, ); + case CursorStyle.dottedVerticalLine: + return DashedCursor( + color: color, + strokeWidth: 2.0, + strokeCap: StrokeCap.round, + ); case CursorStyle.borderLine: return Container( decoration: BoxDecoration( diff --git a/lib/src/render/selection/cursor_widget.dart b/lib/src/render/selection/cursor_widget.dart index bfea2cadd..6316484d4 100644 --- a/lib/src/render/selection/cursor_widget.dart +++ b/lib/src/render/selection/cursor_widget.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:appflowy_editor/src/render/selection/dashed_cursor_painter.dart'; import 'package:appflowy_editor/src/render/selection/selectable.dart'; import 'package:flutter/material.dart'; @@ -85,6 +86,12 @@ class CursorWidgetState extends State { return Container( color: color, ); + case CursorStyle.dottedVerticalLine: + return DashedCursor( + color: color, + strokeWidth: 2.0, + strokeCap: StrokeCap.round, + ); case CursorStyle.borderLine: return Container( decoration: BoxDecoration( diff --git a/lib/src/render/selection/dashed_cursor_painter.dart b/lib/src/render/selection/dashed_cursor_painter.dart new file mode 100644 index 000000000..07447ce36 --- /dev/null +++ b/lib/src/render/selection/dashed_cursor_painter.dart @@ -0,0 +1,61 @@ +import 'package:flutter/material.dart'; + +class DashedCursor extends StatelessWidget { + const DashedCursor({ + super.key, + required this.color, + required this.strokeCap, + required this.strokeWidth, + }); + + final Color color; + final double strokeWidth; + final StrokeCap strokeCap; + + @override + Widget build(BuildContext context) { + return CustomPaint( + painter: DashedCursorPainter( + color: color, + strokeCap: strokeCap, + strokeWidth: strokeWidth, + ), + ); + } +} + +class DashedCursorPainter extends CustomPainter { + DashedCursorPainter({ + required this.color, + required this.strokeCap, + required this.strokeWidth, + }); + + final Color color; + final double strokeWidth; + final StrokeCap strokeCap; + + @override + void paint(Canvas canvas, Size size) { + final paint = Paint() + ..color = color + ..strokeWidth = strokeWidth; + //..strokeCap = strokeCap; + + double height = size.height + 2; + for (double i = 0; i < height; i += 5) { + canvas.drawLine( + Offset(size.width, i), + Offset(size.width, i + 2.5), + paint, + ); + } + } + + @override + bool shouldRepaint(DashedCursorPainter oldDelegate) { + return color != oldDelegate.color || + strokeCap != oldDelegate.strokeCap || + strokeWidth != oldDelegate.strokeWidth; + } +} diff --git a/lib/src/render/selection/selectable.dart b/lib/src/render/selection/selectable.dart index fb5abc552..c6ec6e80a 100644 --- a/lib/src/render/selection/selectable.dart +++ b/lib/src/render/selection/selectable.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; enum CursorStyle { verticalLine, + dottedVerticalLine, borderLine, cover, } diff --git a/lib/src/service/selection/selection_gesture.dart b/lib/src/service/selection/selection_gesture.dart index c0ca0ae1e..5bb719d2a 100644 --- a/lib/src/service/selection/selection_gesture.dart +++ b/lib/src/service/selection/selection_gesture.dart @@ -10,6 +10,7 @@ class SelectionGestureDetector extends StatefulWidget { const SelectionGestureDetector({ super.key, this.child, + this.onTapUp, this.onTapDown, this.onDoubleTapDown, this.onTripleTapDown, @@ -25,6 +26,7 @@ class SelectionGestureDetector extends StatefulWidget { final Widget? child; + final GestureTapUpCallback? onTapUp; final GestureTapDownCallback? onTapDown; final GestureTapDownCallback? onDoubleTapDown; final GestureTapDownCallback? onTripleTapDown; @@ -70,6 +72,7 @@ class SelectionGestureDetectorState extends State { GestureRecognizerFactoryWithHandlers( () => TapGestureRecognizer(), (recognizer) { + recognizer.onTapUp = widget.onTapUp; recognizer.onTapDown = _tapDownDelegate; recognizer.onSecondaryTapDown = widget.onSecondaryTapDown; }, diff --git a/test/new/block_component/table_block_component/table_action_test.dart b/test/new/block_component/table_block_component/table_action_test.dart index 10ffa4ab0..97b02e21a 100644 --- a/test/new/block_component/table_block_component/table_action_test.dart +++ b/test/new/block_component/table_block_component/table_action_test.dart @@ -44,6 +44,7 @@ void main() async { }, ); await editor.dispose(); + await tester.pumpAndSettle(); }); testWidgets('remove row', (tester) async { @@ -79,6 +80,7 @@ void main() async { ); await editor.dispose(); + await tester.pumpAndSettle(); }); testWidgets('remove the last column', (tester) async { @@ -100,6 +102,7 @@ void main() async { expect(tester.editor.document.isEmpty, isTrue); await editor.dispose(); + await tester.pumpAndSettle(); }); testWidgets('remove the last row', (tester) async { @@ -122,6 +125,7 @@ void main() async { expect(tester.editor.document.isEmpty, isTrue); await editor.dispose(); + await tester.pumpAndSettle(); }); testWidgets('duplicate column', (tester) async { @@ -151,6 +155,7 @@ void main() async { ); } await editor.dispose(); + await tester.pumpAndSettle(); }); testWidgets('duplicate row', (tester) async { @@ -180,6 +185,7 @@ void main() async { ); } await editor.dispose(); + await tester.pumpAndSettle(); }); testWidgets('add column', (tester) async { @@ -211,6 +217,7 @@ void main() async { ); expect(tableNode.getColWidth(2), tableNode.config.colDefaultWidth); await editor.dispose(); + await tester.pumpAndSettle(); }); testWidgets('add row', (tester) async { @@ -244,6 +251,7 @@ void main() async { var cell12 = getCellNode(tableNode.node, 1, 2)!; expect(tableNode.getRowHeight(2), cell12.children.first.rect.height + 8); await editor.dispose(); + await tester.pumpAndSettle(); }); testWidgets('set row bg color', (tester) async { @@ -275,6 +283,7 @@ void main() async { ); } await editor.dispose(); + await tester.pumpAndSettle(); }); testWidgets('add column respect row bg color', (tester) async { @@ -314,6 +323,7 @@ void main() async { color, ); await editor.dispose(); + await tester.pumpAndSettle(); }); testWidgets('add row respect column bg color', (tester) async { diff --git a/test/new/block_component/table_block_component/table_block_component_test.dart b/test/new/block_component/table_block_component/table_block_component_test.dart index 1053ae1a3..bb0aa02e9 100644 --- a/test/new/block_component/table_block_component/table_block_component_test.dart +++ b/test/new/block_component/table_block_component/table_block_component_test.dart @@ -28,6 +28,7 @@ void main() async { ParagraphBlockKeys.type, ); await editor.dispose(); + await tester.pumpAndSettle(); }); /*testWidgets('table delete action', (tester) async { diff --git a/test/new/block_component/table_block_component/table_commands_test.dart b/test/new/block_component/table_block_component/table_commands_test.dart index 26735d4e4..e178892e5 100644 --- a/test/new/block_component/table_block_component/table_commands_test.dart +++ b/test/new/block_component/table_block_component/table_commands_test.dart @@ -39,6 +39,7 @@ void main() async { expect(selection.start.path, cell01.childAtIndexOrNull(0)!.path); expect(selection.start.offset, 0); await editor.dispose(); + await tester.pumpAndSettle(); }); testWidgets('enter key on last cell', (tester) async { @@ -68,6 +69,7 @@ void main() async { expect(selection.start.offset, 0); expect(editor.documentRootLen, 2); await editor.dispose(); + await tester.pumpAndSettle(); }); testWidgets('backspace on beginning of cell', (tester) async { @@ -96,6 +98,7 @@ void main() async { expect(selection.start.path, cell10.childAtIndexOrNull(0)!.path); expect(selection.start.offset, 0); await editor.dispose(); + await tester.pumpAndSettle(); }); testWidgets('backspace on multiple cell selection', (tester) async { @@ -178,6 +181,7 @@ void main() async { expect(editor.document.last!.delta?.toPlainText(), 'ting'); await editor.dispose(); + await tester.pumpAndSettle(); }); testWidgets('backspace on whole table in selection', (tester) async { @@ -215,6 +219,7 @@ void main() async { expect(editor.document.last!.delta?.toPlainText(), 'Sting'); await editor.dispose(); + await tester.pumpAndSettle(); }); testWidgets('up arrow key move to above row with same column', @@ -259,6 +264,7 @@ void main() async { expect(selection.start.path, cell00.childAtIndexOrNull(0)!.path); expect(selection.start.offset, 2); await editor.dispose(); + await tester.pumpAndSettle(); }); testWidgets('down arrow key move to down row with same column', @@ -303,6 +309,7 @@ void main() async { expect(selection.start.path, cell01.childAtIndexOrNull(0)!.path); expect(selection.start.offset, 2); await editor.dispose(); + await tester.pumpAndSettle(); }); testWidgets('arrowLeft key on beginning of a cell', (tester) async { @@ -349,6 +356,7 @@ void main() async { expect(selection.start.offset, 0); await editor.dispose(); + await tester.pumpAndSettle(); }); testWidgets('arrowLeft key on middle of a cell', (tester) async { @@ -379,6 +387,7 @@ void main() async { expect(selection.start.offset, 0); await editor.dispose(); + await tester.pumpAndSettle(); }); testWidgets('arrowRight key on beginning of a cell', (tester) async { @@ -409,6 +418,7 @@ void main() async { expect(selection.start.offset, 1); await editor.dispose(); + await tester.pumpAndSettle(); }); testWidgets('arrowRight key on end of a cell', (tester) async { @@ -457,6 +467,7 @@ void main() async { expect(selection.start.offset, 2); await editor.dispose(); + await tester.pumpAndSettle(); }); testWidgets('tab key navigates to next cell', (tester) async { @@ -520,6 +531,7 @@ void main() async { expect(selection.start.offset, 0); await editor.dispose(); + await tester.pumpAndSettle(); }); testWidgets('shift+tab key navigates to the previous cell', (tester) async { @@ -586,6 +598,7 @@ void main() async { expect(selection.start.offset, 0); await editor.dispose(); + await tester.pumpAndSettle(); }); }); } diff --git a/test/new/block_component/table_block_component/table_view_test.dart b/test/new/block_component/table_block_component/table_view_test.dart index fd47c14ff..62504d3d9 100644 --- a/test/new/block_component/table_block_component/table_view_test.dart +++ b/test/new/block_component/table_block_component/table_view_test.dart @@ -46,6 +46,7 @@ void main() async { expect(tableNode.getRowHeight(1), row1beforeHeight); expect(tableNode.getRowHeight(1) < tableNode.getRowHeight(0), true); await editor.dispose(); + await tester.pumpAndSettle(); }); testWidgets('row height changing base on column width', (tester) async { @@ -86,6 +87,7 @@ void main() async { expect(tableNode.getRowHeight(0), row0beforeHeight); await editor.dispose(); + await tester.pumpAndSettle(); }); }); } diff --git a/test/new/block_component/todo_list_block_component/todo_list_power_toggle_test.dart b/test/new/block_component/todo_list_block_component/todo_list_power_toggle_test.dart index b3116a136..883228737 100644 --- a/test/new/block_component/todo_list_block_component/todo_list_power_toggle_test.dart +++ b/test/new/block_component/todo_list_block_component/todo_list_power_toggle_test.dart @@ -57,6 +57,7 @@ void main() async { expect(n3.attributes[TodoListBlockKeys.checked], true); await editor.dispose(); + await tester.pumpAndSettle(); }); }); } diff --git a/test/new/infra/testable_editor.dart b/test/new/infra/testable_editor.dart index d20211d4a..916bb1b18 100644 --- a/test/new/infra/testable_editor.dart +++ b/test/new/infra/testable_editor.dart @@ -177,7 +177,7 @@ class TestableEditor { _ime = null; // Workaround: to wait all the debounce calls expire. // https://github.com/flutter/flutter/issues/11181#issuecomment-568737491 - await tester.pumpAndSettle(const Duration(seconds: 1)); + //await tester.pumpAndSettle(const Duration(seconds: 1)); } void addNode(Node node) { diff --git a/test/new/service/shortcuts/command_shortcut_events/checkbox_event_handler_test.dart b/test/new/service/shortcuts/command_shortcut_events/checkbox_event_handler_test.dart index 3a868c60c..71eb9ba81 100644 --- a/test/new/service/shortcuts/command_shortcut_events/checkbox_event_handler_test.dart +++ b/test/new/service/shortcuts/command_shortcut_events/checkbox_event_handler_test.dart @@ -53,6 +53,7 @@ void main() async { expect(node.attributes[TodoListBlockKeys.checked], false); await editor.dispose(); + await tester.pumpAndSettle(); }); testWidgets( @@ -103,6 +104,7 @@ void main() async { } await editor.dispose(); + await tester.pumpAndSettle(); }); testWidgets( @@ -152,6 +154,7 @@ void main() async { } await editor.dispose(); + await tester.pumpAndSettle(); }); }); } diff --git a/test/new/service/shortcuts/command_shortcut_events/copy_paste_handler_test.dart b/test/new/service/shortcuts/command_shortcut_events/copy_paste_handler_test.dart index 228d3b13f..f215e34ab 100644 --- a/test/new/service/shortcuts/command_shortcut_events/copy_paste_handler_test.dart +++ b/test/new/service/shortcuts/command_shortcut_events/copy_paste_handler_test.dart @@ -49,6 +49,7 @@ void main() async { testWidgets('update selection and execute cut command', (tester) async { await _testCutHandle(tester, Document.fromJson(cutData)); + await tester.pumpAndSettle(); }); }); } @@ -72,6 +73,7 @@ Future _testCutHandle( ); await editor.dispose(); + await tester.pumpAndSettle(); } Future _testHandleCopy(WidgetTester tester, Document document) async { @@ -92,6 +94,7 @@ Future _testHandleCopy(WidgetTester tester, Document document) async { expect(clipBoardData.text, text); await editor.dispose(); + await tester.pumpAndSettle(); } Future _testSameNodeCopyPaste( @@ -121,6 +124,7 @@ Future _testSameNodeCopyPaste( ); await editor.dispose(); + await tester.pumpAndSettle(); } // Future _testNestedNodeCopyPaste( diff --git a/test/new/service/shortcuts/command_shortcut_events/format_style_handler_test.dart b/test/new/service/shortcuts/command_shortcut_events/format_style_handler_test.dart index 63740f9cf..dc6e59939 100644 --- a/test/new/service/shortcuts/command_shortcut_events/format_style_handler_test.dart +++ b/test/new/service/shortcuts/command_shortcut_events/format_style_handler_test.dart @@ -213,4 +213,5 @@ Future _testUpdateTextStyleByCommandX( } await editor.dispose(); + await tester.pumpAndSettle(); } diff --git a/test/new/service/shortcuts/command_shortcut_events/markdown_commands_test.dart b/test/new/service/shortcuts/command_shortcut_events/markdown_commands_test.dart index cbbd13218..b63374bbb 100644 --- a/test/new/service/shortcuts/command_shortcut_events/markdown_commands_test.dart +++ b/test/new/service/shortcuts/command_shortcut_events/markdown_commands_test.dart @@ -179,4 +179,5 @@ Future _testUpdateTextStyleByCommandX( } await editor.dispose(); + await tester.pumpAndSettle(); } diff --git a/test/new/service/shortcuts/command_shortcut_events/paste_command_test.dart b/test/new/service/shortcuts/command_shortcut_events/paste_command_test.dart index 33539647c..eaffe9681 100644 --- a/test/new/service/shortcuts/command_shortcut_events/paste_command_test.dart +++ b/test/new/service/shortcuts/command_shortcut_events/paste_command_test.dart @@ -57,6 +57,7 @@ void main() async { AppFlowyClipboard.mockSetData(null); await editor.dispose(); + await tester.pumpAndSettle(); }, ); @@ -118,6 +119,7 @@ Future _testHandleCopyMultiplePaste( thirdParagraph, ); await editor.dispose(); + await tester.pumpAndSettle(); } Future _testHandleCopyPaste( @@ -146,6 +148,7 @@ Future _testHandleCopyPaste( expect(editor.document.toJson(), plainTextJson); await editor.dispose(); + await tester.pumpAndSettle(); } const paragraphData = { diff --git a/test/new/service/shortcuts/command_shortcut_events/redo_undo_handler_test.dart b/test/new/service/shortcuts/command_shortcut_events/redo_undo_handler_test.dart index de540a088..2a17669ca 100644 --- a/test/new/service/shortcuts/command_shortcut_events/redo_undo_handler_test.dart +++ b/test/new/service/shortcuts/command_shortcut_events/redo_undo_handler_test.dart @@ -60,6 +60,7 @@ Future _testRedoWithoutUndo(WidgetTester tester) async { expect(editor.documentRootLen, 3); await editor.dispose(); + await tester.pumpAndSettle(); } Future _testWithTextFormattingBold(WidgetTester tester) async { @@ -113,6 +114,7 @@ Future _testWithTextFormattingBold(WidgetTester tester) async { expect(result, true); await editor.dispose(); + await tester.pumpAndSettle(); } Future _testWithTextFormattingItalics(WidgetTester tester) async { @@ -165,6 +167,7 @@ Future _testWithTextFormattingItalics(WidgetTester tester) async { expect(allItalics, true); await editor.dispose(); + await tester.pumpAndSettle(); } Future _testWithTextFormattingUnderline(WidgetTester tester) async { @@ -217,6 +220,7 @@ Future _testWithTextFormattingUnderline(WidgetTester tester) async { expect(allUnderline, true); await editor.dispose(); + await tester.pumpAndSettle(); } Future _testBackspaceUndoRedo( @@ -248,6 +252,7 @@ Future _testBackspaceUndoRedo( expect(editor.documentRootLen, 2); await editor.dispose(); + await tester.pumpAndSettle(); } Future _pressUndoCommand(TestableEditor editor) async { diff --git a/test/new/service/shortcuts/command_shortcut_events/toggle_color_commands_test.dart b/test/new/service/shortcuts/command_shortcut_events/toggle_color_commands_test.dart index b9bcd70f3..a464f884e 100644 --- a/test/new/service/shortcuts/command_shortcut_events/toggle_color_commands_test.dart +++ b/test/new/service/shortcuts/command_shortcut_events/toggle_color_commands_test.dart @@ -138,4 +138,5 @@ Future _testUpdateTextColorByCommandX( } await editor.dispose(); + await tester.pumpAndSettle(); } diff --git a/test/new/service/shortcuts/command_shortcut_events/white_space_handler_test.dart b/test/new/service/shortcuts/command_shortcut_events/white_space_handler_test.dart index 47b2969fc..7e06165c9 100644 --- a/test/new/service/shortcuts/command_shortcut_events/white_space_handler_test.dart +++ b/test/new/service/shortcuts/command_shortcut_events/white_space_handler_test.dart @@ -270,6 +270,7 @@ void main() async { expect(node.delta!.toPlainText(), 'AppFlowy'); await editor.dispose(); + await tester.pumpAndSettle(); }); testWidgets('AppFlowy > nothing changes', (tester) async { diff --git a/test/service/scroll_service_test.dart b/test/service/scroll_service_test.dart index 26d61cec2..743d829b8 100644 --- a/test/service/scroll_service_test.dart +++ b/test/service/scroll_service_test.dart @@ -27,6 +27,7 @@ void main() async { ); expect(itemFinder, findsOneWidget); await editor.dispose(); + await tester.pumpAndSettle(); }); }); } diff --git a/test/service/selection_service_test.dart b/test/service/selection_service_test.dart index 59955168e..e58cd000a 100644 --- a/test/service/selection_service_test.dart +++ b/test/service/selection_service_test.dart @@ -159,6 +159,7 @@ void main() async { ); await editor.dispose(); + await tester.pumpAndSettle(); }); testWidgets('Block selection and then single tap', (tester) async {