diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/selection_service.dart b/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/selection_service.dart index 6e63de8747..b522aa9cef 100644 --- a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/selection_service.dart +++ b/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/selection_service.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:appflowy_editor/src/flutter/overlay.dart'; import 'package:appflowy_editor/src/infra/log.dart'; import 'package:appflowy_editor/src/service/context_menu/built_in_context_menu_item.dart'; @@ -121,6 +123,9 @@ class _AppFlowySelectionState extends State EditorState get editorState => widget.editorState; + // Toolbar + Timer? _toolbarTimer; + @override void initState() { super.initState(); @@ -144,6 +149,7 @@ class _AppFlowySelectionState extends State clearSelection(); WidgetsBinding.instance.removeObserver(this); currentSelection.removeListener(_onSelectionChange); + _clearToolbar(); super.dispose(); } @@ -236,7 +242,7 @@ class _AppFlowySelectionState extends State // clear cursor areas // hide toolbar - editorState.service.toolbarService?.hide(); + // editorState.service.toolbarService?.hide(); // clear context menu _clearContextMenu(); @@ -482,13 +488,8 @@ class _AppFlowySelectionState extends State Overlay.of(context)?.insertAll(_selectionAreas); - if (toolbarOffset != null && layerLink != null) { - editorState.service.toolbarService?.showInOffset( - toolbarOffset, - alignment!, - layerLink, - ); - } + // show toolbar + _showToolbarWithDelay(toolbarOffset, layerLink, alignment!); } void _updateCursorAreas(Position position) { @@ -502,6 +503,7 @@ class _AppFlowySelectionState extends State currentSelectedNodes = [node]; _showCursor(node, position); + _clearToolbar(); } void _showCursor(Node node, Position position) { @@ -628,6 +630,40 @@ class _AppFlowySelectionState extends State _scrollUpOrDownIfNeeded(); } + void _showToolbarWithDelay( + Offset? toolbarOffset, + LayerLink? layerLink, + Alignment alignment, { + Duration delay = const Duration(milliseconds: 400), + }) { + if (toolbarOffset == null && layerLink == null) { + _clearToolbar(); + return; + } + if (_toolbarTimer?.isActive ?? false) { + _toolbarTimer?.cancel(); + } + _toolbarTimer = Timer( + delay, + () { + if (toolbarOffset != null && layerLink != null) { + editorState.service.toolbarService?.showInOffset( + toolbarOffset, + alignment, + layerLink, + ); + } + }, + ); + } + + void _clearToolbar() { + editorState.service.toolbarService?.hide(); + if (_toolbarTimer?.isActive ?? false) { + _toolbarTimer?.cancel(); + } + } + void _showDebugLayerIfNeeded({Offset? offset}) { // remove false to show debug overlay. // if (kDebugMode && false) { diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/toolbar_service.dart b/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/toolbar_service.dart index 762d550803..9fd8ca3648 100644 --- a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/toolbar_service.dart +++ b/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/toolbar_service.dart @@ -7,7 +7,11 @@ import 'package:appflowy_editor/src/extensions/object_extensions.dart'; abstract class AppFlowyToolbarService { /// Show the toolbar widget beside the offset. - void showInOffset(Offset offset, Alignment alignment, LayerLink layerLink); + void showInOffset( + Offset offset, + Alignment alignment, + LayerLink layerLink, + ); /// Hide the toolbar widget. void hide(); @@ -45,7 +49,11 @@ class _FlowyToolbarState extends State } @override - void showInOffset(Offset offset, Alignment alignment, LayerLink layerLink) { + void showInOffset( + Offset offset, + Alignment alignment, + LayerLink layerLink, + ) { hide(); final items = _filterItems(toolbarItems); if (items.isEmpty) { diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/test/render/rich_text/toolbar_rich_text_test.dart b/frontend/appflowy_flutter/packages/appflowy_editor/test/render/rich_text/toolbar_rich_text_test.dart index b9e774b351..54f9ed0455 100644 --- a/frontend/appflowy_flutter/packages/appflowy_editor/test/render/rich_text/toolbar_rich_text_test.dart +++ b/frontend/appflowy_flutter/packages/appflowy_editor/test/render/rich_text/toolbar_rich_text_test.dart @@ -25,6 +25,7 @@ void main() async { await editor.updateSelection(h1); + await tester.pumpAndSettle(const Duration(milliseconds: 500)); expect(find.byType(ToolbarWidget), findsOneWidget); final h1Button = find.byWidgetPredicate((widget) { @@ -52,6 +53,7 @@ void main() async { end: Position(path: [0], offset: singleLineText.length)); await editor.updateSelection(h2); + await tester.pumpAndSettle(const Duration(milliseconds: 500)); expect(find.byType(ToolbarWidget), findsOneWidget); final h2Button = find.byWidgetPredicate((widget) { @@ -77,6 +79,7 @@ void main() async { end: Position(path: [0], offset: singleLineText.length)); await editor.updateSelection(h3); + await tester.pumpAndSettle(const Duration(milliseconds: 500)); expect(find.byType(ToolbarWidget), findsOneWidget); final h3Button = find.byWidgetPredicate((widget) { @@ -104,6 +107,7 @@ void main() async { end: Position(path: [0], offset: singleLineText.length)); await editor.updateSelection(underline); + await tester.pumpAndSettle(const Duration(milliseconds: 500)); expect(find.byType(ToolbarWidget), findsOneWidget); final underlineButton = find.byWidgetPredicate((widget) { if (widget is ToolbarItemWidget) { @@ -132,6 +136,7 @@ void main() async { end: Position(path: [0], offset: singleLineText.length)); await editor.updateSelection(bold); + await tester.pumpAndSettle(const Duration(milliseconds: 500)); expect(find.byType(ToolbarWidget), findsOneWidget); final boldButton = find.byWidgetPredicate((widget) { if (widget is ToolbarItemWidget) { @@ -159,6 +164,7 @@ void main() async { end: Position(path: [0], offset: singleLineText.length)); await editor.updateSelection(italic); + await tester.pumpAndSettle(const Duration(milliseconds: 500)); expect(find.byType(ToolbarWidget), findsOneWidget); final italicButton = find.byWidgetPredicate((widget) { if (widget is ToolbarItemWidget) { @@ -187,6 +193,7 @@ void main() async { await editor.updateSelection(strikeThrough); + await tester.pumpAndSettle(const Duration(milliseconds: 500)); expect(find.byType(ToolbarWidget), findsOneWidget); final strikeThroughButton = find.byWidgetPredicate((widget) { if (widget is ToolbarItemWidget) { @@ -214,6 +221,7 @@ void main() async { end: Position(path: [0], offset: singleLineText.length)); await editor.updateSelection(code); + await tester.pumpAndSettle(const Duration(milliseconds: 500)); expect(find.byType(ToolbarWidget), findsOneWidget); final codeButton = find.byWidgetPredicate((widget) { if (widget is ToolbarItemWidget) { @@ -250,6 +258,7 @@ void main() async { end: Position(path: [0], offset: singleLineText.length)); await editor.updateSelection(quote); + await tester.pumpAndSettle(const Duration(milliseconds: 500)); expect(find.byType(ToolbarWidget), findsOneWidget); final quoteButton = find.byWidgetPredicate((widget) { if (widget is ToolbarItemWidget) { @@ -276,6 +285,7 @@ void main() async { end: Position(path: [0], offset: singleLineText.length)); await editor.updateSelection(bulletList); + await tester.pumpAndSettle(const Duration(milliseconds: 500)); expect(find.byType(ToolbarWidget), findsOneWidget); final bulletListButton = find.byWidgetPredicate((widget) { if (widget is ToolbarItemWidget) { @@ -306,6 +316,7 @@ void main() async { end: Position(path: [0], offset: singleLineText.length)); await editor.updateSelection(selection); + await tester.pumpAndSettle(const Duration(milliseconds: 500)); expect(find.byType(ToolbarWidget), findsOneWidget); final highlightButton = find.byWidgetPredicate((widget) { if (widget is ToolbarItemWidget) { @@ -343,6 +354,7 @@ void main() async { ); await editor.updateSelection(selection); + await tester.pumpAndSettle(const Duration(milliseconds: 500)); expect(find.byType(ToolbarWidget), findsOneWidget); final colorButton = find.byWidgetPredicate((widget) { if (widget is ToolbarItemWidget) { diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/test/service/internal_key_event_handlers/format_style_handler_test.dart b/frontend/appflowy_flutter/packages/appflowy_editor/test/service/internal_key_event_handlers/format_style_handler_test.dart index 0cb4c71600..222a7efe1b 100644 --- a/frontend/appflowy_flutter/packages/appflowy_editor/test/service/internal_key_event_handlers/format_style_handler_test.dart +++ b/frontend/appflowy_flutter/packages/appflowy_editor/test/service/internal_key_event_handlers/format_style_handler_test.dart @@ -245,6 +245,7 @@ Future _testLinkMenuInSingleTextSelection(WidgetTester tester) async { await editor.updateSelection(selection); // show toolbar + await tester.pumpAndSettle(const Duration(milliseconds: 500)); expect(find.byType(ToolbarWidget), findsOneWidget); // trigger the link menu diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/test/service/toolbar_service_test.dart b/frontend/appflowy_flutter/packages/appflowy_editor/test/service/toolbar_service_test.dart index 6c2ab09157..86cd29705d 100644 --- a/frontend/appflowy_flutter/packages/appflowy_editor/test/service/toolbar_service_test.dart +++ b/frontend/appflowy_flutter/packages/appflowy_editor/test/service/toolbar_service_test.dart @@ -24,6 +24,7 @@ void main() async { ); await editor.updateSelection(selection); + await tester.pumpAndSettle(const Duration(milliseconds: 500)); expect(find.byType(ToolbarWidget), findsOneWidget); // no link item @@ -72,6 +73,7 @@ void main() async { await editor.updateSelection( Selection.single(path: [0], startOffset: 0, endOffset: text.length), ); + await tester.pumpAndSettle(const Duration(milliseconds: 500)); expect(find.byType(ToolbarWidget), findsOneWidget); void testHighlight(bool expectedValue) { @@ -138,6 +140,7 @@ void main() async { await editor.updateSelection( Selection.single(path: [0], startOffset: 0, endOffset: text.length), ); + await tester.pumpAndSettle(const Duration(milliseconds: 500)); expect(find.byType(ToolbarWidget), findsOneWidget); var itemWidget = _itemWidgetForId(tester, 'appflowy.toolbar.h1'); expect(itemWidget.isHighlight, true); @@ -145,6 +148,7 @@ void main() async { await editor.updateSelection( Selection.single(path: [1], startOffset: 0, endOffset: text.length), ); + await tester.pumpAndSettle(const Duration(milliseconds: 500)); expect(find.byType(ToolbarWidget), findsOneWidget); itemWidget = _itemWidgetForId(tester, 'appflowy.toolbar.quote'); expect(itemWidget.isHighlight, true); @@ -152,6 +156,7 @@ void main() async { await editor.updateSelection( Selection.single(path: [2], startOffset: 0, endOffset: text.length), ); + await tester.pumpAndSettle(const Duration(milliseconds: 500)); expect(find.byType(ToolbarWidget), findsOneWidget); itemWidget = _itemWidgetForId(tester, 'appflowy.toolbar.bulleted_list'); expect(itemWidget.isHighlight, true); @@ -183,6 +188,7 @@ void main() async { await editor.updateSelection( Selection.single(path: [2], startOffset: text.length, endOffset: 0), ); + await tester.pumpAndSettle(const Duration(milliseconds: 500)); expect(find.byType(ToolbarWidget), findsOneWidget); expect( _itemWidgetForId(tester, 'appflowy.toolbar.h1').isHighlight, @@ -199,6 +205,7 @@ void main() async { end: Position(path: [1], offset: 0), ), ); + await tester.pumpAndSettle(const Duration(milliseconds: 500)); expect(find.byType(ToolbarWidget), findsOneWidget); expect( _itemWidgetForId(tester, 'appflowy.toolbar.bold').isHighlight, @@ -211,6 +218,7 @@ void main() async { end: Position(path: [0], offset: 0), ), ); + await tester.pumpAndSettle(const Duration(milliseconds: 500)); expect(find.byType(ToolbarWidget), findsOneWidget); expect( _itemWidgetForId(tester, 'appflowy.toolbar.bold').isHighlight,