Repository: superlistapp/super_editor

Issue:
[BUG] - long press without selection opens the toolbar AND closes it again immidiately
**Package Version**
super_editor, GitHub, stable

**User Info**
individual

**To Reproduce**
Steps to reproduce the behavior:
1. Run the example app
2. Long press anywhere (with the intent of inserting someting, so without an expanded selection)
4. See the toolbar to insert someting from the clipboard appear and immidiately close again

**Minimal Reproduction Code**
Simply use the example app, no change needed.

**Actual behavior**
The Toolbar opens and closes, making it impossible to tap one of the buttons

**Expected behavior**
The long-press should open the toolbar and only close it when taping somewhere else again. That allows actually pressing a button on the toolbar.

**Platform**
Android, did not investigate others, may be relevant there too.

**Flutter version**
Flutter 3.27.1, Dart 3.6.0

**Screenshots**

See the video:

https://github.com/user-attachments/assets/3f9abe14-f4ef-44e6-8022-edefc43fdccb


**Additional context**

At first glance, it looks like the following like is incorrect and should be removed to fix this:

https://github.com/superlistapp/super_editor/blob/583d978b53896462be3e9f3a1bf3dd4607e55a53/super_editor/lib/src/default_editor/document_gestures_touch_android.dart#L731

with more context visible, the following line seems to be incorrect, which is also what i did in the video:

```dart
  void _onTapUp(TapUpDetails details) {
    // Stop waiting for a long-press to start.
    _tapDownLongPressTimer?.cancel();

    // Cancel any on-going long-press.
    if (_isLongPressInProgress) {
      _longPressStrategy = null;
      _magnifierGlobalOffset.value = null;
      _showAndHideEditingControlsAfterTapSelection(didTapOnExistingSelection: false); // ← * SEEMS INCORRECT HERE *
      return;
    }
```




Fix patch:
diff --git a/super_editor/lib/src/default_editor/document_gestures_touch_android.dart b/super_editor/lib/src/default_editor/document_gestures_touch_android.dart
index 76ad4e194..120607276 100644
--- a/super_editor/lib/src/default_editor/document_gestures_touch_android.dart
+++ b/super_editor/lib/src/default_editor/document_gestures_touch_android.dart
@@ -32,6 +32,7 @@ import 'package:super_editor/src/infrastructure/platforms/mobile_documents.dart'
 import 'package:super_editor/src/infrastructure/signal_notifier.dart';
 import 'package:super_editor/src/infrastructure/sliver_hybrid_stack.dart';
 import 'package:super_editor/src/infrastructure/touch_controls.dart';
+import 'package:super_keyboard/super_keyboard.dart';
 
 import '../infrastructure/document_gestures.dart';
 import '../infrastructure/document_gestures_interaction_overrides.dart';
@@ -673,6 +674,9 @@ class _AndroidDocumentTouchInteractorState extends State<AndroidDocumentTouchInt
   }
 
   void _onDocumentChange(_) {
+    // The user might start typing when the toolbar is visible. Hide it.
+    _controlsController!.hideToolbar();
+
     onNextFrame((_) {
       _ensureSelectionExtentIsVisible();
     });
@@ -751,9 +755,9 @@ class _AndroidDocumentTouchInteractorState extends State<AndroidDocumentTouchInt
 
     // Cancel any on-going long-press.
     if (_isLongPressInProgress) {
+      _onLongPressEnd();
       _longPressStrategy = null;
       _magnifierGlobalOffset.value = null;
-      _showAndHideEditingControlsAfterTapSelection(didTapOnExistingSelection: false);
       return;
     }
 
@@ -987,7 +991,8 @@ class _AndroidDocumentTouchInteractorState extends State<AndroidDocumentTouchInt
         ..hideMagnifier()
         ..blinkCaret();
 
-      if (didTapOnExistingSelection && _isKeyboardOpen) {
+      if (didTapOnExistingSelection &&
+          SuperKeyboard.instance.mobileGeometry.value.keyboardState == KeyboardState.open) {
         // Toggle the toolbar display when the user taps on the collapsed caret,
         // or on top of an existing selection.
         //
@@ -1002,16 +1007,6 @@ class _AndroidDocumentTouchInteractorState extends State<AndroidDocumentTouchInt
     }
   }
 
-  /// Returns `true` if we *think* the software keyboard is currently open, or
-  /// `false` otherwise.
-  ///
-  /// We say "think" because Flutter doesn't report this info to us. Instead, we
-  /// inspect the bottom insets on the window, and we assume any insets greater than
-  /// zero means a keyboard is visible.
-  bool get _isKeyboardOpen {
-    return MediaQuery.viewInsetsOf(context).bottom > 0;
-  }
-
   void _onPanStart(DragStartDetails details) {
     // Stop waiting for a long-press to start, if a long press isn't already in-progress.
     _tapDownLongPressTimer?.cancel();
diff --git a/super_editor/lib/src/default_editor/document_gestures_touch_ios.dart b/super_editor/lib/src/default_editor/document_gestures_touch_ios.dart
index ba42c4d07..b0aeb3f2b 100644
--- a/super_editor/lib/src/default_editor/document_gestures_touch_ios.dart
+++ b/super_editor/lib/src/default_editor/document_gestures_touch_ios.dart
@@ -32,6 +32,7 @@ import 'package:super_editor/src/infrastructure/platforms/platform.dart';
 import 'package:super_editor/src/infrastructure/signal_notifier.dart';
 import 'package:super_editor/src/infrastructure/sliver_hybrid_stack.dart';
 import 'package:super_editor/src/infrastructure/touch_controls.dart';
+import 'package:super_keyboard/super_keyboard.dart';
 
 import '../infrastructure/document_gestures.dart';
 import '../infrastructure/document_gestures_interaction_overrides.dart';
@@ -268,7 +269,6 @@ class IosDocumentTouchInteractor extends StatefulWidget {
     this.openKeyboardWhenTappingExistingSelection = true,
     this.openKeyboardOnSelectionChange = true,
     required this.openSoftwareKeyboard,
-    required this.isImeConnected,
     required this.scrollController,
     required this.dragHandleAutoScroller,
     required this.fillViewport,
@@ -294,10 +294,6 @@ class IosDocumentTouchInteractor extends StatefulWidget {
   /// A callback that should open the software keyboard when invoked.
   final VoidCallback openSoftwareKeyboard;
 
-  /// A [ValueListenable] that should notify this widget when the IME connects
-  /// and disconnects.
-  final ValueListenable<bool> isImeConnected;
-
   /// Optional list of handlers that respond to taps on content, e.g., opening
   /// a link when the user taps on text with a link attribution.
   ///
@@ -666,7 +662,8 @@ class _IosDocumentTouchInteractorState extends State<IosDocumentTouchInteractor>
           selection.extent.nodeId == docPosition.nodeId &&
           selection.extent.nodePosition.isEquivalentTo(docPosition.nodePosition);
 
-      if (didTapOnExistingSelection && widget.isImeConnected.value) {
+      if (didTapOnExistingSelection &&
+          SuperKeyboard.instance.mobileGeometry.value.keyboardState == KeyboardState.open) {
         // Toggle the toolbar display when the user taps on the collapsed caret,
         // or on top of an existing selection.
         //
diff --git a/super_editor/lib/src/default_editor/super_editor.dart b/super_editor/lib/src/default_editor/super_editor.dart
index 011d3a6c3..35f4ce951 100644
--- a/super_editor/lib/src/default_editor/super_editor.dart
+++ b/super_editor/lib/src/default_editor/super_editor.dart
@@ -924,7 +924,6 @@ class SuperEditorState extends State<SuperEditor> {
           openKeyboardWhenTappingExistingSelection: widget.selectionPolicies.openKeyboardWhenTappingExistingSelection,
           openKeyboardOnSelectionChange: widget.imePolicies.openKeyboardOnSelectionChange,
           openSoftwareKeyboard: _openSoftwareKeyboard,
-          isImeConnected: _isImeConnected,
           contentTapHandlers: [
             ..._contentTapHandlers ?? [],
             for (final plugin in widget.plugins) //


Test patch:
diff --git a/super_editor/test/super_editor/mobile/super_editor_android_overlay_controls_test.dart b/super_editor/test/super_editor/mobile/super_editor_android_overlay_controls_test.dart
index e7dbe4377..9e0d089ff 100644
--- a/super_editor/test/super_editor/mobile/super_editor_android_overlay_controls_test.dart
+++ b/super_editor/test/super_editor/mobile/super_editor_android_overlay_controls_test.dart
@@ -8,6 +8,7 @@ import 'package:golden_toolkit/golden_toolkit.dart';
 import 'package:super_editor/src/infrastructure/platforms/android/selection_handles.dart';
 import 'package:super_editor/super_editor.dart';
 import 'package:super_editor/super_editor_test.dart';
+import 'package:super_keyboard/test/keyboard_simulator.dart';
 import 'package:super_text_layout/super_text_layout.dart';
 
 import '../../test_runners.dart';
@@ -174,6 +175,41 @@ void main() {
       expect(SuperEditorInspector.isMobileMagnifierVisible(), isFalse);
     });
 
+    testWidgetsOnAndroid("shows toolbar when long pressing on an empty paragraph and hides it after typing",
+        (tester) async {
+      await tester //
+          .createDocument()
+          .withSingleEmptyParagraph()
+          .pump();
+
+      // The decision about showing the toolbar depends on the keyboard visibility.
+      // Simulate the keyboard being visible immediately after the IME is connected.
+      TestSuperKeyboard.install(id: '1', tester, keyboardAnimationTime: Duration.zero);
+      addTearDown(() => TestSuperKeyboard.uninstall('1'));
+
+      // Ensure the toolbar is not visible.
+      expect(SuperEditorInspector.isMobileToolbarVisible(), isFalse);
+
+      // Long press to show the toolbar.
+      final gesture = await tester.longPressDownInParagraph('1', 0);
+
+      // Ensure the toolbar is visible.
+      expect(SuperEditorInspector.isMobileToolbarVisible(), isTrue);
+
+      // Release the finger.
+      await gesture.up();
+      await tester.pump();
+
+      // Ensure the toolbar is still visible.
+      expect(SuperEditorInspector.isMobileToolbarVisible(), isTrue);
+
+      // Type a character to hide the toolbar.
+      await tester.typeImeText('a');
+
+      // Ensure the toolbar is not visible.
+      expect(SuperEditorInspector.isMobileToolbarVisible(), isFalse);
+    });
+
     testWidgetsOnAndroid("shows magnifier when dragging expanded handle", (tester) async {
       await _pumpSingleParagraphApp(tester);
 
diff --git a/super_editor/test/super_editor/mobile/super_editor_ios_overlay_controls_test.dart b/super_editor/test/super_editor/mobile/super_editor_ios_overlay_controls_test.dart
index b8477d631..7e151c085 100644
--- a/super_editor/test/super_editor/mobile/super_editor_ios_overlay_controls_test.dart
+++ b/super_editor/test/super_editor/mobile/super_editor_ios_overlay_controls_test.dart
@@ -4,6 +4,7 @@ import 'package:flutter_test_robots/flutter_test_robots.dart';
 import 'package:flutter_test_runners/flutter_test_runners.dart';
 import 'package:super_editor/super_editor.dart';
 import 'package:super_editor/super_editor_test.dart';
+import 'package:super_keyboard/test/keyboard_simulator.dart';
 import 'package:super_text_layout/super_text_layout.dart';
 
 import '../../test_runners.dart';
@@ -133,6 +134,41 @@ void main() {
       expect(SuperEditorInspector.isMobileMagnifierVisible(), isFalse);
     });
 
+    testWidgetsOnIos("shows toolbar when long pressing on an empty paragraph and hides it after typing",
+        (tester) async {
+      await tester //
+          .createDocument()
+          .withSingleEmptyParagraph()
+          .pump();
+
+      // The decision about showing the toolbar depends on the keyboard visibility.
+      // Simulate the keyboard being visible immediately after the IME is connected.
+      TestSuperKeyboard.install(id: '1', tester, keyboardAnimationTime: Duration.zero);
+      addTearDown(() => TestSuperKeyboard.uninstall('1'));
+
+      // Ensure the toolbar is not visible.
+      expect(SuperEditorInspector.isMobileToolbarVisible(), isFalse);
+
+      // Long press, this shouldn't show the toolbar.
+      final gesture = await tester.longPressDownInParagraph('1', 0);
+
+      // Ensure the toolbar is not visible yet.
+      expect(SuperEditorInspector.isMobileToolbarVisible(), isFalse);
+
+      // Release the finger.
+      await gesture.up();
+      await tester.pump();
+
+      // Ensure the toolbar is visible.
+      expect(SuperEditorInspector.isMobileToolbarVisible(), isTrue);
+
+      // Type a character to hide the toolbar.
+      await tester.typeImeText('a');
+
+      // Ensure the toolbar is not visible.
+      expect(SuperEditorInspector.isMobileToolbarVisible(), isFalse);
+    });
+
     testWidgetsOnIos("does not show toolbar upon first tap", (tester) async {
       await tester //
           .createDocument()