From d74f2f8f1760f1e6be96e833169c9990a5ae9b13 Mon Sep 17 00:00:00 2001 From: Jayaprakash Date: Tue, 23 Jan 2024 23:38:36 +0530 Subject: [PATCH 1/9] feat: enable to drag-and-drop text in editor --- .../selection/block_selection_area.dart | 55 +++- .../selection/block_selection_container.dart | 5 + .../bulleted_list_block_component.dart | 37 +++ .../divider_block_component.dart | 1 + .../heading_block_component.dart | 37 +++ .../image_block_component.dart | 2 + .../numbered_list_block_component.dart | 37 +++ .../paragraph_block_component.dart | 27 ++ .../quote_block_component.dart | 37 +++ .../rich_text/appflowy_rich_text.dart | 26 +- .../table_block_component.dart | 1 + .../todo_list_block_component.dart | 37 +++ .../editor/command/selection_commands.dart | 2 +- .../selection/desktop_selection_service.dart | 254 +++++++++++++++++- lib/src/editor_state.dart | 99 +++++++ lib/src/render/selection/cursor.dart | 9 +- lib/src/render/selection/cursor_widget.dart | 4 + .../selection/dashed_cursor_painter.dart | 61 +++++ lib/src/render/selection/selectable.dart | 1 + 19 files changed, 716 insertions(+), 16 deletions(-) create mode 100644 lib/src/render/selection/dashed_cursor_painter.dart 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..37cc9d813 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,8 @@ class BlockSelectionArea extends StatefulWidget { // get the selection from the listenable final ValueListenable listenable; + final ValueListenable? dragAndDropListenable; + // the color of the cursor final Color cursorColor; @@ -60,6 +65,7 @@ class _BlockSelectionAreaState extends State { debugLabel: 'cursor_${widget.node.path}', ); + List? prevDragAndDropSelectionRects; // keep the previous cursor rect to avoid unnecessary rebuild Rect? prevCursorRect; // keep the previous selection rects to avoid unnecessary rebuild @@ -86,11 +92,29 @@ class _BlockSelectionAreaState extends State { @override Widget build(BuildContext context) { - return ValueListenableBuilder( + final listenableChild = ValueListenableBuilder( key: ValueKey(widget.node.id + widget.supportTypes.toString()), 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) { @@ -156,6 +180,15 @@ class _BlockSelectionAreaState extends State { }), child: const SizedBox.shrink(), ); + + return widget.dragAndDropListenable != null + ? ValueListenableBuilder( + valueListenable: widget.dragAndDropListenable!, + builder: (context, value, child) { + return listenableChild; + }, + ) + : listenableChild; } void _updateSelectionIfNeeded() { @@ -163,11 +196,29 @@ class _BlockSelectionAreaState extends State { return; } + Selection? dragAndDropSelection; + if (widget.dragAndDropListenable != null) { + dragAndDropSelection = widget.dragAndDropListenable!.value?.normalized; + } + final selection = widget.listenable.value?.normalized; final path = widget.node.path; + 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..4833b0859 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 _onCursorStlyeChange() { + cursorStyle = editorState.cursorStyle; + + shouldCurSorBlink = cursorStyle != CursorStyle.dottedVerticalLine; + } + + @override + void initState() { + super.initState(); + editorState.cursorStyleNotifier.addListener(_onCursorStlyeChange); + } + + @override + void dispose() { + editorState.cursorStyleNotifier.removeListener(_onCursorStlyeChange); + 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..bb2b6deaa 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 _onCursorStlyeChange() { + cursorStyle = editorState.cursorStyle; + + shouldCurSorBlink = cursorStyle != CursorStyle.dottedVerticalLine; + } + + @override + void initState() { + super.initState(); + editorState.cursorStyleNotifier.addListener(_onCursorStlyeChange); + } + + @override + void dispose() { + editorState.cursorStyleNotifier.removeListener(_onCursorStlyeChange); + 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..499533b03 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,7 @@ 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..1767962f9 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 _onCursorStlyeChange() { + cursorStyle = editorState.cursorStyle; + + shouldCurSorBlink = cursorStyle != CursorStyle.dottedVerticalLine; + } + + @override + void initState() { + super.initState(); + editorState.cursorStyleNotifier.addListener(_onCursorStlyeChange); + } + + @override + void dispose() { + editorState.cursorStyleNotifier.removeListener(_onCursorStlyeChange); + 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..90c433598 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(_onCursorStlyeChange); _onSelectionChange(); } @override void dispose() { editorState.selectionNotifier.removeListener(_onSelectionChange); + editorState.cursorStyleNotifier.removeListener(_onCursorStlyeChange); super.dispose(); } @@ -140,6 +160,12 @@ class _ParagraphBlockComponentWidgetState } } + void _onCursorStlyeChange() { + 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..4a0908852 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 _onCursorStlyeChange() { + cursorStyle = editorState.cursorStyle; + + shouldCurSorBlink = cursorStyle != CursorStyle.dottedVerticalLine; + } + + @override + void initState() { + super.initState(); + editorState.cursorStyleNotifier.addListener(_onCursorStlyeChange); + } + + @override + void dispose() { + editorState.cursorStyleNotifier.removeListener(_onCursorStlyeChange); + 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..8fd20ed9e 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 _onCursorStlyeChange() { + cursorStyle = editorState.cursorStyle; + + shouldCurSorBlink = cursorStyle != CursorStyle.dottedVerticalLine; + } + + @override + void initState() { + super.initState(); + editorState.cursorStyleNotifier.addListener(_onCursorStlyeChange); + } + + @override + void dispose() { + editorState.cursorStyleNotifier.removeListener(_onCursorStlyeChange); + 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/editor_component/service/selection/desktop_selection_service.dart b/lib/src/editor/editor_component/service/selection/desktop_selection_service.dart index 318024b2c..720159375 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 @@ -39,6 +39,7 @@ class _DesktopSelectionServiceWidgetState @override ValueNotifier currentSelection = ValueNotifier(null); + ValueNotifier currentDragAndDropSelection = ValueNotifier(null); @override List get currentSelectedNodes => editorState.getSelectedNodes(); @@ -51,6 +52,14 @@ class _DesktopSelectionServiceWidgetState Position? _panStartPosition; + final Set> _dragAndDropSelectables = {}; + final Set _dragAndDropSelectionRects = {}; + bool _isCursorPointValid = false; + + // cursor position calculated during drag and drop op. + double cursorX = 0; + double cursorY = 0; + late EditorState editorState = Provider.of( context, listen: false, @@ -111,6 +120,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 +241,87 @@ class _DesktopSelectionServiceWidgetState throw UnimplementedError(); } + // resets the presets after drag and drop op. + void reset() { + cursorX = cursorY = 0; + + _cachedDragAndDropSelectionRects = null; + _dragAndDropSelectionRects.clear(); + + _cachedDragAndDropSelectables = null; + _dragAndDropSelectables.clear(); + + clearSelection(); + updateCursorStyle(CursorStyle.verticalLine); + updateMouseCursorStyle(SystemMouseCursors.text); + updateDragAndDropSelection(null); + } + + bool isCursorInSelection(double dx, double dy, Rect rect) { + if (dx > rect.right || + dx < rect.left || + dy < rect.top || + dy > rect.bottom) { + return false; + } + return true; + } + + 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 topRightOffset = selectable.localToGlobal(Offset(right, top)); + final rightBottomOffset = selectable.localToGlobal(Offset(right, bottom)); + + left = leftTopOffset.dx; + top = leftTopOffset.dy; + right = topRightOffset.dx; + bottom = rightBottomOffset.dy; + + return Rect.fromLTRB(left, top, right, bottom); + } + + 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; + } + void _onTapDown(TapDownDetails details) { _clearContextMenu(); @@ -223,12 +329,32 @@ 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)) { + _isCursorPointValid = true; + return; + } + } + } + if (selectable == null) { // Clear old start offset _panStartOffset = null; @@ -256,16 +382,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 +414,8 @@ class _DesktopSelectionServiceWidgetState start: selectable.start(), end: selectable.end(), ); + dragAndDropSelectable = selectable; + updateDragAndDropSelection(selection); updateSelection(selection); } @@ -316,8 +451,22 @@ class _DesktopSelectionServiceWidgetState return; } - final panEndOffset = details.globalPosition; final dy = editorState.service.scrollService?.dy; + + if (_isCursorPointValid) { + final selection = currentDragAndDropSelection.value; + if (selection == null) { + return; + } + + cursorX = details.globalPosition.dx; + cursorY = details.globalPosition.dy; + + panCursor(details.globalPosition, selection); + return; + } + + final panEndOffset = details.globalPosition; final panStartOffset = dy == null ? _panStartOffset! : _panStartOffset!.translate(0, _panStartScrollDy! - dy); @@ -330,7 +479,9 @@ class _DesktopSelectionServiceWidgetState final start = _panStartPosition!; final end = last.getSelectionInRange(panStartOffset, panEndOffset).end; final selection = Selection(start: start, end: end); + dragAndDropSelectable = last; updateSelection(selection); + updateDragAndDropSelection(selection); } editorState.service.scrollService?.startAutoScroll( @@ -343,6 +494,19 @@ 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, + ); + } + } + reset(); + _isCursorPointValid = false; + } } void _updateSelection() { @@ -465,6 +629,92 @@ class _DesktopSelectionServiceWidgetState return min.clamp(start, end); } + void panCursor(Offset offset, Selection selection) { + 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; + + List textInSelection = editorState.getTextInSelection(selection); + int len = 0; + + if (selection.isBackward) { + textInSelection = textInSelection.reversed.toList(); + } + + for (final text in textInSelection) { + 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, + ); + + newCursorPosition = Selection.collapsed( + Position( + path: toNode.path, + offset: len, + ), + ); + } + + editorState.deleteSelection(selection); + + if (fromNode != toNode) { + newCursorPosition = Selection.collapsed( + Position( + path: toNode.path, + offset: len, + ), + ); + } + + // update the cursor position to the + // last of the edited [toNode] path after the op. + 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..1e8899553 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. /// @@ -40,6 +41,7 @@ enum SelectionUpdateReason { enum SelectionType { inline, block, + dragAndDrop, } enum TransactionTime { @@ -112,12 +114,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 selection notifier of the editor. + final PropertyValueNotifier dragAndDropSelectionNotifier = + PropertyValueNotifier(null); + + /// The selection of the editor. + Selection? get dragAndDropSelection => dragAndDropSelectionNotifier.value; + + /// Sets the 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 +263,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.dragAndDrop; + 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..c09c1776a 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( @@ -88,7 +95,7 @@ class CursorState extends State { width: size.width, height: size.height, color: color.withOpacity(0.2), - ); + ); } } } diff --git a/lib/src/render/selection/cursor_widget.dart b/lib/src/render/selection/cursor_widget.dart index bfea2cadd..fd3915a5e 100644 --- a/lib/src/render/selection/cursor_widget.dart +++ b/lib/src/render/selection/cursor_widget.dart @@ -85,6 +85,10 @@ class CursorWidgetState extends State { return Container( color: color, ); + case CursorStyle.dottedVerticalLine: + return Container( + color: color, + ); 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, } From 0f425ccb18722b20c6ea99718233bf2711f556b5 Mon Sep 17 00:00:00 2001 From: Jayaprakash Date: Wed, 24 Jan 2024 01:04:33 +0530 Subject: [PATCH 2/9] fix: Enhance cursor position check with "negativeOffset" --- .../selection/block_selection_area.dart | 18 +--- .../selection/desktop_selection_service.dart | 97 +++++++++++-------- lib/src/render/selection/cursor.dart | 2 +- lib/src/render/selection/cursor_widget.dart | 5 +- 4 files changed, 67 insertions(+), 55 deletions(-) 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 37cc9d813..060b09675 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 @@ -65,6 +65,8 @@ 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; @@ -92,7 +94,7 @@ class _BlockSelectionAreaState extends State { @override Widget build(BuildContext context) { - final listenableChild = ValueListenableBuilder( + return ValueListenableBuilder( key: ValueKey(widget.node.id + widget.supportTypes.toString()), valueListenable: widget.listenable, builder: ((context, value, child) { @@ -180,30 +182,20 @@ class _BlockSelectionAreaState extends State { }), child: const SizedBox.shrink(), ); - - return widget.dragAndDropListenable != null - ? ValueListenableBuilder( - valueListenable: widget.dragAndDropListenable!, - builder: (context, value, child) { - return listenableChild; - }, - ) - : listenableChild; } void _updateSelectionIfNeeded() { 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; } - final selection = widget.listenable.value?.normalized; - final path = widget.node.path; - if (dragAndDropSelection != null) { if (widget.supportTypes.contains(BlockSelectionType.dragAndDrop)) { final rects = widget.delegate.getRectsInSelection(dragAndDropSelection); 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 720159375..5e77b7b50 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,8 @@ import 'package:flutter/material.dart' hide Overlay, OverlayEntry; import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; +const negativeOffset = 4; + class DesktopSelectionServiceWidget extends StatefulWidget { const DesktopSelectionServiceWidget({ super.key, @@ -52,14 +54,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, @@ -241,7 +282,7 @@ class _DesktopSelectionServiceWidgetState throw UnimplementedError(); } - // resets the presets after drag and drop op. + // Resets the presets for drag and drop selection after the operation void reset() { cursorX = cursorY = 0; @@ -257,6 +298,7 @@ class _DesktopSelectionServiceWidgetState updateDragAndDropSelection(null); } + /// returns true, if the cursor is inside the selection rect bool isCursorInSelection(double dx, double dy, Rect rect) { if (dx > rect.right || dx < rect.left || @@ -267,6 +309,10 @@ class _DesktopSelectionServiceWidgetState 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!); @@ -280,48 +326,17 @@ class _DesktopSelectionServiceWidgetState } final leftTopOffset = selectable.localToGlobal(Offset(left, top)); - final topRightOffset = selectable.localToGlobal(Offset(right, top)); final rightBottomOffset = selectable.localToGlobal(Offset(right, bottom)); - left = leftTopOffset.dx; - top = leftTopOffset.dy; - right = topRightOffset.dx; - bottom = rightBottomOffset.dy; + // Added negative offset to eliminate rect boundaries + left = leftTopOffset.dx - negativeOffset; + top = leftTopOffset.dy - negativeOffset; + right = rightBottomOffset.dx - negativeOffset; + bottom = rightBottomOffset.dy - negativeOffset; return Rect.fromLTRB(left, top, right, bottom); } - 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; - } - void _onTapDown(TapDownDetails details) { _clearContextMenu(); @@ -480,8 +495,8 @@ class _DesktopSelectionServiceWidgetState final end = last.getSelectionInRange(panStartOffset, panEndOffset).end; final selection = Selection(start: start, end: end); dragAndDropSelectable = last; - updateSelection(selection); updateDragAndDropSelection(selection); + updateSelection(selection); } editorState.service.scrollService?.startAutoScroll( @@ -705,13 +720,15 @@ class _DesktopSelectionServiceWidgetState newCursorPosition = Selection.collapsed( Position( path: toNode.path, - offset: len, + offset: cursorPosition.startIndex + len, ), ); } // update the cursor position to the - // last of the edited [toNode] path after the op. + // last node or the cursor point of the + // edited [toNode] path after + // the drag and drop operation updateSelection(newCursorPosition); } diff --git a/lib/src/render/selection/cursor.dart b/lib/src/render/selection/cursor.dart index c09c1776a..08810c52a 100644 --- a/lib/src/render/selection/cursor.dart +++ b/lib/src/render/selection/cursor.dart @@ -95,7 +95,7 @@ class CursorState extends State { width: size.width, height: size.height, color: color.withOpacity(0.2), - ); + ); } } } diff --git a/lib/src/render/selection/cursor_widget.dart b/lib/src/render/selection/cursor_widget.dart index fd3915a5e..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'; @@ -86,8 +87,10 @@ class CursorWidgetState extends State { color: color, ); case CursorStyle.dottedVerticalLine: - return Container( + return DashedCursor( color: color, + strokeWidth: 2.0, + strokeCap: StrokeCap.round, ); case CursorStyle.borderLine: return Container( From 46bb6f3e9e7df48c560cd2a9f4dc4df8b524dfa4 Mon Sep 17 00:00:00 2001 From: Jayaprakash Date: Wed, 24 Jan 2024 01:12:44 +0530 Subject: [PATCH 3/9] refactor: changed code comments --- .../service/selection/desktop_selection_service.dart | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) 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 5e77b7b50..e3b3a9b7a 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 @@ -314,8 +314,9 @@ class _DesktopSelectionServiceWidgetState /// 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!); + final rects = selectable.getRectsInSelection( + currentDragAndDropSelection.value!, + ); double left = 0.0, top = 0.0, right = 0.0, bottom = 0.0; for (final rect in rects) { @@ -725,9 +726,8 @@ class _DesktopSelectionServiceWidgetState ); } - // update the cursor position to the - // last node or the cursor point of the - // edited [toNode] path after + // 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); } From 3b2e3b56fe80f16442f75c4d835aa1f761d5ee74 Mon Sep 17 00:00:00 2001 From: Jayaprakash Date: Wed, 24 Jan 2024 03:50:53 +0530 Subject: [PATCH 4/9] fix: resolved the insertion order for multi-line selection --- lib/src/editor/command/text_commands.dart | 5 ++ .../selection/desktop_selection_service.dart | 75 ++++++++++++------- 2 files changed, 51 insertions(+), 29 deletions(-) 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 e3b3a9b7a..b325e9aff 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,7 +8,8 @@ import 'package:flutter/material.dart' hide Overlay, OverlayEntry; import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; -const negativeOffset = 4; +const int negativeVerticalOffset = 12; +const int negativeHorizontalOffset = 10; class DesktopSelectionServiceWidget extends StatefulWidget { const DesktopSelectionServiceWidget({ @@ -59,10 +60,10 @@ class _DesktopSelectionServiceWidgetState final Set> _dragAndDropSelectables = {}; /// stores the calculated rect for each selectable - /// object in dragAndDropSelectables. + /// object in `dragAndDropSelectables`. final Set _dragAndDropSelectionRects = {}; - /// true, if the cursor is inside the selected + /// `true`, if the cursor is inside the selected /// rect on drag and drop operation. bool _isCursorPointValid = false; @@ -298,19 +299,35 @@ class _DesktopSelectionServiceWidgetState updateDragAndDropSelection(null); } - /// returns true, if the cursor is inside the selection rect - bool isCursorInSelection(double dx, double dy, Rect rect) { - if (dx > rect.right || - dx < rect.left || - dy < rect.top || - dy > rect.bottom) { + /// 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 + /// 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) { @@ -330,10 +347,10 @@ class _DesktopSelectionServiceWidgetState final rightBottomOffset = selectable.localToGlobal(Offset(right, bottom)); // Added negative offset to eliminate rect boundaries - left = leftTopOffset.dx - negativeOffset; - top = leftTopOffset.dy - negativeOffset; - right = rightBottomOffset.dx - negativeOffset; - bottom = rightBottomOffset.dy - negativeOffset; + left = leftTopOffset.dx + negativeHorizontalOffset; + top = leftTopOffset.dy + negativeVerticalOffset; + right = rightBottomOffset.dx - negativeHorizontalOffset; + bottom = rightBottomOffset.dy - negativeVerticalOffset; return Rect.fromLTRB(left, top, right, bottom); } @@ -364,7 +381,12 @@ class _DesktopSelectionServiceWidgetState cursorY = offset.dy; for (final rect in dragAndDropSelectionRects) { - if (isCursorInSelection(cursorX, cursorY, rect)) { + if (isCursorInSelection( + cursorX, + cursorY, + rect, + removeNegativeOffset: true, + )) { _isCursorPointValid = true; return; } @@ -518,6 +540,7 @@ class _DesktopSelectionServiceWidgetState currentDragAndDropSelection.value, currentSelection.value, ); + break; } } reset(); @@ -669,14 +692,14 @@ class _DesktopSelectionServiceWidgetState final toNode = editorState.getNodeAtPath(cursorPosition.start.path); if (fromNode == null || toNode == null) return; - List textInSelection = editorState.getTextInSelection(selection); + final textInSelection = editorState.getTextInSelection(selection); int len = 0; - if (selection.isBackward) { - textInSelection = textInSelection.reversed.toList(); - } - - for (final text in textInSelection) { + 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); } @@ -706,18 +729,12 @@ class _DesktopSelectionServiceWidgetState start: newStartPosition, end: newEndPosition, ); - - newCursorPosition = Selection.collapsed( - Position( - path: toNode.path, - offset: len, - ), - ); } editorState.deleteSelection(selection); - if (fromNode != toNode) { + if (fromNode != toNode || + (fromNode == toNode && cursorPosition.startIndex < end)) { newCursorPosition = Selection.collapsed( Position( path: toNode.path, From 36df37a97d14409ef855bfda9a0e0cf3902dd3e7 Mon Sep 17 00:00:00 2001 From: Jayaprakash Date: Wed, 24 Jan 2024 04:31:45 +0530 Subject: [PATCH 5/9] chore: add comments for properties to enhance code documentation --- .../selection/block_selection_area.dart | 2 ++ .../bulleted_list_block_component.dart | 10 +++++----- .../heading_block_component.dart | 10 +++++----- .../numbered_list_block_component.dart | 10 +++++----- .../paragraph_block_component.dart | 10 +++++----- .../quote_block_component/quote_block_component.dart | 10 +++++----- .../todo_list_block_component.dart | 10 +++++----- .../service/selection/desktop_selection_service.dart | 12 +++++------- lib/src/editor_state.dart | 6 +++--- 9 files changed, 40 insertions(+), 40 deletions(-) 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 060b09675..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 @@ -40,6 +40,8 @@ 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 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 4833b0859..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 @@ -120,25 +120,25 @@ class _BulletedListBlockComponentWidgetState @override bool get shouldCursorBlink => _shouldCursorBlink; - set shouldCurSorBlink(bool value) { + set shouldCursorBlink(bool value) { _shouldCursorBlink = value; } - void _onCursorStlyeChange() { + void _onCursorStyleChange() { cursorStyle = editorState.cursorStyle; - shouldCurSorBlink = cursorStyle != CursorStyle.dottedVerticalLine; + shouldCursorBlink = cursorStyle != CursorStyle.dottedVerticalLine; } @override void initState() { super.initState(); - editorState.cursorStyleNotifier.addListener(_onCursorStlyeChange); + editorState.cursorStyleNotifier.addListener(_onCursorStyleChange); } @override void dispose() { - editorState.cursorStyleNotifier.removeListener(_onCursorStlyeChange); + editorState.cursorStyleNotifier.removeListener(_onCursorStyleChange); super.dispose(); } 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 bb2b6deaa..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 @@ -133,25 +133,25 @@ class _HeadingBlockComponentWidgetState @override bool get shouldCursorBlink => _shouldCursorBlink; - set shouldCurSorBlink(bool value) { + set shouldCursorBlink(bool value) { _shouldCursorBlink = value; } - void _onCursorStlyeChange() { + void _onCursorStyleChange() { cursorStyle = editorState.cursorStyle; - shouldCurSorBlink = cursorStyle != CursorStyle.dottedVerticalLine; + shouldCursorBlink = cursorStyle != CursorStyle.dottedVerticalLine; } @override void initState() { super.initState(); - editorState.cursorStyleNotifier.addListener(_onCursorStlyeChange); + editorState.cursorStyleNotifier.addListener(_onCursorStyleChange); } @override void dispose() { - editorState.cursorStyleNotifier.removeListener(_onCursorStlyeChange); + editorState.cursorStyleNotifier.removeListener(_onCursorStyleChange); super.dispose(); } 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 1767962f9..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 @@ -126,25 +126,25 @@ class _NumberedListBlockComponentWidgetState @override bool get shouldCursorBlink => _shouldCursorBlink; - set shouldCurSorBlink(bool value) { + set shouldCursorBlink(bool value) { _shouldCursorBlink = value; } - void _onCursorStlyeChange() { + void _onCursorStyleChange() { cursorStyle = editorState.cursorStyle; - shouldCurSorBlink = cursorStyle != CursorStyle.dottedVerticalLine; + shouldCursorBlink = cursorStyle != CursorStyle.dottedVerticalLine; } @override void initState() { super.initState(); - editorState.cursorStyleNotifier.addListener(_onCursorStlyeChange); + editorState.cursorStyleNotifier.addListener(_onCursorStyleChange); } @override void dispose() { - editorState.cursorStyleNotifier.removeListener(_onCursorStlyeChange); + editorState.cursorStyleNotifier.removeListener(_onCursorStyleChange); super.dispose(); } 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 90c433598..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 @@ -125,7 +125,7 @@ class _ParagraphBlockComponentWidgetState @override bool get shouldCursorBlink => _shouldCursorBlink; - set shouldCurSorBlink(bool value) { + set shouldCursorBlink(bool value) { _shouldCursorBlink = value; } @@ -133,14 +133,14 @@ class _ParagraphBlockComponentWidgetState void initState() { super.initState(); editorState.selectionNotifier.addListener(_onSelectionChange); - editorState.cursorStyleNotifier.addListener(_onCursorStlyeChange); + editorState.cursorStyleNotifier.addListener(_onCursorStyleChange); _onSelectionChange(); } @override void dispose() { editorState.selectionNotifier.removeListener(_onSelectionChange); - editorState.cursorStyleNotifier.removeListener(_onCursorStlyeChange); + editorState.cursorStyleNotifier.removeListener(_onCursorStyleChange); super.dispose(); } @@ -160,10 +160,10 @@ class _ParagraphBlockComponentWidgetState } } - void _onCursorStlyeChange() { + void _onCursorStyleChange() { cursorStyle = editorState.cursorStyle; - shouldCurSorBlink = cursorStyle != CursorStyle.dottedVerticalLine; + shouldCursorBlink = cursorStyle != CursorStyle.dottedVerticalLine; } @override 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 4a0908852..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 @@ -119,25 +119,25 @@ class _QuoteBlockComponentWidgetState extends State @override bool get shouldCursorBlink => _shouldCursorBlink; - set shouldCurSorBlink(bool value) { + set shouldCursorBlink(bool value) { _shouldCursorBlink = value; } - void _onCursorStlyeChange() { + void _onCursorStyleChange() { cursorStyle = editorState.cursorStyle; - shouldCurSorBlink = cursorStyle != CursorStyle.dottedVerticalLine; + shouldCursorBlink = cursorStyle != CursorStyle.dottedVerticalLine; } @override void initState() { super.initState(); - editorState.cursorStyleNotifier.addListener(_onCursorStlyeChange); + editorState.cursorStyleNotifier.addListener(_onCursorStyleChange); } @override void dispose() { - editorState.cursorStyleNotifier.removeListener(_onCursorStlyeChange); + editorState.cursorStyleNotifier.removeListener(_onCursorStyleChange); super.dispose(); } 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 8fd20ed9e..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 @@ -143,25 +143,25 @@ class _TodoListBlockComponentWidgetState @override bool get shouldCursorBlink => _shouldCursorBlink; - set shouldCurSorBlink(bool value) { + set shouldCursorBlink(bool value) { _shouldCursorBlink = value; } - void _onCursorStlyeChange() { + void _onCursorStyleChange() { cursorStyle = editorState.cursorStyle; - shouldCurSorBlink = cursorStyle != CursorStyle.dottedVerticalLine; + shouldCursorBlink = cursorStyle != CursorStyle.dottedVerticalLine; } @override void initState() { super.initState(); - editorState.cursorStyleNotifier.addListener(_onCursorStlyeChange); + editorState.cursorStyleNotifier.addListener(_onCursorStyleChange); } @override void dispose() { - editorState.cursorStyleNotifier.removeListener(_onCursorStlyeChange); + editorState.cursorStyleNotifier.removeListener(_onCursorStyleChange); super.dispose(); } 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 b325e9aff..4d084b485 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 @@ -492,15 +492,10 @@ class _DesktopSelectionServiceWidgetState final dy = editorState.service.scrollService?.dy; if (_isCursorPointValid) { - final selection = currentDragAndDropSelection.value; - if (selection == null) { - return; - } - cursorX = details.globalPosition.dx; cursorY = details.globalPosition.dy; - panCursor(details.globalPosition, selection); + updateCursorPosition(details.globalPosition); return; } @@ -668,7 +663,10 @@ class _DesktopSelectionServiceWidgetState return min.clamp(start, end); } - void panCursor(Offset offset, Selection selection) { + void updateCursorPosition(Offset offset) { + final selection = currentDragAndDropSelection.value; + if (selection == null) return; + final node = getNodeInOffset(offset); final selectable = node?.selectable; if (selectable == null) { diff --git a/lib/src/editor_state.dart b/lib/src/editor_state.dart index 1e8899553..564c72f0f 100644 --- a/lib/src/editor_state.dart +++ b/lib/src/editor_state.dart @@ -132,14 +132,14 @@ class EditorState { mouseCursorStyleNotifier.value = cursorStyle; } - /// The selection notifier of the editor. + /// The drag and drop selection notifier of the editor. final PropertyValueNotifier dragAndDropSelectionNotifier = PropertyValueNotifier(null); - /// The selection of the editor. + /// The drag and drop selection of the editor. Selection? get dragAndDropSelection => dragAndDropSelectionNotifier.value; - /// Sets the selection of the editor. + /// Sets the drag and drop selection of the editor. set dragAndDropSelection(Selection? value) { // clear the toggled style when the selection is changed. toggledStyle.clear(); From eca6c598bfd2cb9d4f9899568c3c50f60ab1dddc Mon Sep 17 00:00:00 2001 From: Jayaprakash Date: Wed, 24 Jan 2024 04:46:18 +0530 Subject: [PATCH 6/9] chore: remove unnecessary editor-state `SelectionType` property --- lib/src/editor_state.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/src/editor_state.dart b/lib/src/editor_state.dart index 564c72f0f..103be0bf4 100644 --- a/lib/src/editor_state.dart +++ b/lib/src/editor_state.dart @@ -41,7 +41,6 @@ enum SelectionUpdateReason { enum SelectionType { inline, block, - dragAndDrop, } enum TransactionTime { @@ -271,7 +270,7 @@ class EditorState { final completer = Completer(); if (reason == SelectionUpdateReason.uiEvent) { - dragAndDropSelectionType = SelectionType.dragAndDrop; + dragAndDropSelectionType = SelectionType.inline; WidgetsBinding.instance.addPostFrameCallback( (timeStamp) => completer.complete(), ); From 1ec1db5f9c4377a1db896674cd5c712f846dc320 Mon Sep 17 00:00:00 2001 From: Jayaprakash Date: Wed, 24 Jan 2024 05:04:37 +0530 Subject: [PATCH 7/9] feat: adds `onTapUp` event in desktop selection service --- .../service/selection/desktop_selection_service.dart | 7 +++++++ lib/src/service/selection/selection_gesture.dart | 3 +++ 2 files changed, 10 insertions(+) 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 4d084b485..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 @@ -145,6 +145,7 @@ class _DesktopSelectionServiceWidgetState onPanStart: _onPanStart, onPanUpdate: _onPanUpdate, onPanEnd: _onPanEnd, + onTapUp: _onTapUp, onTapDown: _onTapDown, onSecondaryTapDown: _onSecondaryTapDown, onDoubleTapDown: _onDoubleTapDown, @@ -355,6 +356,12 @@ class _DesktopSelectionServiceWidgetState return Rect.fromLTRB(left, top, right, bottom); } + void _onTapUp(TapUpDetails details) { + if (_isCursorPointValid) { + reset(); + } + } + void _onTapDown(TapDownDetails details) { _clearContextMenu(); 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; }, From e356bc3714ce8bfd705d096fb0d26cb9718e9a39 Mon Sep 17 00:00:00 2001 From: Jayaprakash Date: Thu, 25 Jan 2024 01:32:55 +0530 Subject: [PATCH 8/9] fix: resolve `pendingFrames` and `pumpAndSettle` timeout errors in tests --- .../table_block_component/table_action_test.dart | 10 ++++++++++ .../table_block_component_test.dart | 1 + .../table_block_component/table_commands_test.dart | 13 +++++++++++++ .../table_block_component/table_view_test.dart | 2 ++ .../todo_list_power_toggle_test.dart | 1 + test/new/infra/testable_editor.dart | 2 +- .../checkbox_event_handler_test.dart | 3 +++ .../copy_paste_handler_test.dart | 4 ++++ .../format_style_handler_test.dart | 1 + .../markdown_commands_test.dart | 1 + .../command_shortcut_events/paste_command_test.dart | 3 +++ .../redo_undo_handler_test.dart | 5 +++++ .../toggle_color_commands_test.dart | 1 + .../white_space_handler_test.dart | 1 + test/service/scroll_service_test.dart | 1 + test/service/selection_service_test.dart | 1 + 16 files changed, 49 insertions(+), 1 deletion(-) 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 { From cde0f60a730097eea7f64de9c98a5f512a3bec68 Mon Sep 17 00:00:00 2001 From: Jayaprakash Date: Thu, 25 Jan 2024 01:43:52 +0530 Subject: [PATCH 9/9] refactor: formatted code file using dart formatter --- .../image_block_component/image_block_component.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 499533b03..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 @@ -197,7 +197,8 @@ class ImageBlockComponentWidgetState extends State node: node, delegate: this, listenable: editorState.selectionNotifier, - dragAndDropListenable: editorState.dragAndDropSelectionNotifier, + dragAndDropListenable: + editorState.dragAndDropSelectionNotifier, cursorColor: editorState.editorStyle.cursorColor, selectionColor: editorState.editorStyle.selectionColor, child: child!,