singerdmx / flutter-quill

Rich text editor for Flutter
https://pub.dev/packages/flutter_quill
MIT License
2.53k stars 809 forks source link

regression, when using a mouse on Android, the right button no longer works #2167

Closed jonasbernardo closed 5 days ago

jonasbernardo commented 2 weeks ago

Is there an existing issue for this?

Flutter Quill version

10.5.0

Steps to reproduce

select any text and press the right mouse button

Expected results

text selection option appears

Actual results

nothing happens

Code sample

Code sample ```dart [Paste your code here] ```

Additional Context

Screenshots / Video demonstration [Upload media here]

https://github.com/user-attachments/assets/6fd0422b-957e-4602-9ed0-372884b1c2ff

Logs ```console [Paste your logs here] ```
jonasbernardo commented 2 weeks ago

Before I was using version 10.1.2 and it worked perfectly

jonasbernardo commented 2 weeks ago

@CatHood0 @AtlasAutocode

AtlasAutocode commented 2 weeks ago

There have been a lot of changes since 10.1.2 It would be very helpful if you can narrow down when the problem was introduced. In your yaml file you can set a specific version by removing the caret symbol (^) and doing a pub get (I also verify that the correct version was loaded)

flutter_quill: 10.3.0

Increment through the releases till you find where the problem was introduced. I suggest trying 10.3 first and then 10.2 or 10.4 depending on your result.

Since I don't develop for mobile, there's a limit on what I can contribute. On desktop I cannot reproduce this issue. I have also been caught out by not running flutter pub upgrade - sometimes the versions get out of sync and cause unexpected problems.

jonasbernardo commented 2 weeks ago
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';

import '../../common/utils/platform.dart';
import '../../document/attribute.dart';
import '../../document/nodes/leaf.dart';
import '../editor.dart';
import '../raw_editor/raw_editor.dart';
import 'text/text_selection.dart';

typedef CustomStyleBuilder = TextStyle Function(Attribute attribute);

typedef CustomRecognizerBuilder = GestureRecognizer? Function(
    Attribute attribute, Leaf leaf);

/// Delegate interface for the [EditorTextSelectionGestureDetectorBuilder].
///
/// The interface is usually implemented by textfield implementations wrapping
/// [EditableText], that use a [EditorTextSelectionGestureDetectorBuilder]
/// to build a [EditorTextSelectionGestureDetector] for their [EditableText].
/// The delegate provides the builder with information about the current state
/// of the textfield.
/// Based on these information, the builder adds the correct gesture handlers
/// to the gesture detector.
///
/// See also:
///
///  * [TextField], which implements this delegate for the Material textfield.
///  * [CupertinoTextField], which implements this delegate for the Cupertino
///    textfield.
abstract class EditorTextSelectionGestureDetectorBuilderDelegate {
  /// [GlobalKey] to the [EditableText] for which the
  /// [EditorTextSelectionGestureDetectorBuilder] will build
  /// a [EditorTextSelectionGestureDetector].
  GlobalKey<EditorState> get editableTextKey;

  /// Whether the textfield should respond to force presses.
  bool get forcePressEnabled;

  /// Whether the user may select text in the textfield.
  bool get selectionEnabled;
}

/// Builds a [EditorTextSelectionGestureDetector] to wrap an [EditableText].
///
/// The class implements sensible defaults for many user interactions
/// with an [EditableText] (see the documentation of the various gesture handler
/// methods, e.g. [onTapDown], [onForcePressStart], etc.). Subclasses of
/// [EditorTextSelectionGestureDetectorBuilder] can change the behavior
/// performed in responds to these gesture events by overriding
/// the corresponding handler methods of this class.
///
/// The resulting [EditorTextSelectionGestureDetector] to wrap an [EditableText]
/// is obtained by calling [buildGestureDetector].
///
/// See also:
///
///  * [TextField], which uses a subclass to implement the Material-specific
///    gesture logic of an [EditableText].
///  * [CupertinoTextField], which uses a subclass to implement the
///    Cupertino-specific gesture logic of an [EditableText].
class EditorTextSelectionGestureDetectorBuilder {
  /// Creates a [EditorTextSelectionGestureDetectorBuilder].
  ///
  /// The [delegate] must not be null.
  EditorTextSelectionGestureDetectorBuilder(
      {required this.delegate, this.detectWordBoundary = true});

  /// The delegate for this [EditorTextSelectionGestureDetectorBuilder].
  ///
  /// The delegate provides the builder with information about what actions can
  /// currently be performed on the textfield. Based on this, the builder adds
  /// the correct gesture handlers to the gesture detector.
  @protected
  final EditorTextSelectionGestureDetectorBuilderDelegate delegate;

  /// Whether to show the selection toolbar.
  ///
  /// It is based on the signal source when a [onTapDown] is called. This getter
  /// will return true if current [onTapDown] event is triggered by a touch or
  /// a stylus.
  bool shouldShowSelectionToolbar = true;
  PointerDeviceKind? kind;

  /// Check if the selection toolbar should show.
  ///
  /// If mouse is used, the toolbar should only show when right click.
  /// Else, it should show when the selection is enabled.
  bool checkSelectionToolbarShouldShow({required bool isAdditionalAction}) {
    if (kind != PointerDeviceKind.mouse) {
      return shouldShowSelectionToolbar;
    }
    return shouldShowSelectionToolbar && isAdditionalAction;
  }

  bool detectWordBoundary = true;

  /// The [State] of the [EditableText] for which the builder will provide a
  /// [EditorTextSelectionGestureDetector].
  @protected
  EditorState? get editor => delegate.editableTextKey.currentState;

  /// The [RenderObject] of the [EditableText] for which the builder will
  /// provide a [EditorTextSelectionGestureDetector].
  @protected
  RenderEditor? get renderEditor => editor?.renderEditor;

  /// Whether the Shift key was pressed when the most recent [PointerDownEvent]
  /// was tracked by the [BaseTapAndDragGestureRecognizer].
  bool _isShiftPressed = false;

  /// The viewport offset pixels of any [Scrollable] containing the
  /// [RenderEditable] at the last drag start.
  double _dragStartScrollOffset = 0;

  /// The viewport offset pixels of the [RenderEditable] at the last drag start.
  double _dragStartViewportOffset = 0;

  double get _scrollPosition {
    final scrollableState = delegate.editableTextKey.currentContext == null
        ? null
        : Scrollable.maybeOf(delegate.editableTextKey.currentContext!);
    return scrollableState == null ? 0.0 : scrollableState.position.pixels;
  }

  // For tap + drag gesture on iOS, whether the position where the drag started
  // was on the previous TextSelection. iOS uses this value to determine if
  // the cursor should move on drag update.
  //
  TextSelection? _dragStartSelection;

  // If the drag started on the previous selection then the cursor will move on
  // drag update. If the drag did not start on the previous selection then the
  // cursor will not move on drag update.
  bool? _dragBeganOnPreviousSelection;

  /// Returns true if lastSecondaryTapDownPosition was on selection.
  bool get _lastSecondaryTapWasOnSelection {
    assert(renderEditor?.lastSecondaryTapDownPosition != null);
    if (renderEditor?.selection == null) {
      return false;
    }
    renderEditor?.lastSecondaryTapDownPosition;
    final textPosition = renderEditor?.getPositionForOffset(
      renderEditor!.lastSecondaryTapDownPosition!,
    );

    if (textPosition == null) return false;

    return renderEditor!.selection.start <= textPosition.offset &&
        renderEditor!.selection.end >= textPosition.offset;
  }

  /// Returns true if position was on selection.
  bool _positionOnSelection(Offset position, TextSelection? targetSelection) {
    if (targetSelection == null) return false;

    final textPosition = renderEditor?.getPositionForOffset(position);

    if (textPosition == null) return false;

    return targetSelection.start <= textPosition.offset &&
        targetSelection.end >= textPosition.offset;
  }

  // Expand the selection to the given global position.
  //
  // Either base or extent will be moved to the last tapped position, whichever
  // is closest. The selection will never shrink or pivot, only grow.
  //
  // If fromSelection is given, will expand from that selection instead of the
  // current selection in renderEditable.
  //
  // See also:
  //
  //   * [_extendSelection], which is similar but pivots the selection around
  //     the base.
  void _expandSelection(Offset offset, SelectionChangedCause cause,
      [TextSelection? fromSelection]) {
    final tappedPosition = renderEditor!.getPositionForOffset(offset);
    final selection = fromSelection ?? renderEditor!.selection;
    final baseIsCloser = (tappedPosition.offset - selection.baseOffset).abs() <
        (tappedPosition.offset - selection.extentOffset).abs();
    final nextSelection = selection.copyWith(
      baseOffset: baseIsCloser ? selection.extentOffset : selection.baseOffset,
      extentOffset: tappedPosition.offset,
    );

    editor?.userUpdateTextEditingValue(
        editor!.textEditingValue.copyWith(selection: nextSelection), cause);
  }

  // Extend the selection to the given global position.
  //
  // Holds the base in place and moves the extent.
  //
  // See also:
  //
  //   * [_expandSelection], which is similar but always increases the size of
  //     the selection.
  void _extendSelection(Offset offset, SelectionChangedCause cause) {
    assert(renderEditor?.selection.baseOffset != null);

    final tappedPosition = renderEditor!.getPositionForOffset(offset);
    final selection = renderEditor!.selection;
    final nextSelection = selection.copyWith(
      extentOffset: tappedPosition.offset,
    );

    editor?.userUpdateTextEditingValue(
        editor!.textEditingValue.copyWith(selection: nextSelection), cause);
  }

  /// Handler for [TextSelectionGestureDetector.onTapTrackStart].
  ///
  /// See also:
  ///
  ///  * [TextSelectionGestureDetector.onTapTrackStart], which triggers this
  ///    callback.
  @protected
  void onTapTrackStart() {
    _isShiftPressed = HardwareKeyboard.instance.logicalKeysPressed
        .intersection(<LogicalKeyboardKey>{
      LogicalKeyboardKey.shiftLeft,
      LogicalKeyboardKey.shiftRight
    }).isNotEmpty;
  }

  /// Handler for [TextSelectionGestureDetector.onTapTrackReset].
  ///
  /// See also:
  ///
  ///  * [TextSelectionGestureDetector.onTapTrackReset], which triggers this
  ///    callback.
  @protected
  void onTapTrackReset() {
    _isShiftPressed = false;
  }

  /// Handler for [EditorTextSelectionGestureDetector.onTapDown].
  ///
  /// By default, it forwards the tap to [RenderEditable.handleTapDown] and sets
  /// [shouldShowSelectionToolbar] to true if the tap was initiated by a finger
  /// or stylus.
  ///
  /// See also:
  ///
  ///  * [EditorTextSelectionGestureDetector.onTapDown],
  ///  which triggers this callback.
  @protected
  void onTapDown(TapDragDownDetails details) {
    if (!delegate.selectionEnabled) return;
    renderEditor!
        .handleTapDown(TapDownDetails(globalPosition: details.globalPosition));
    final kind = details.kind;
    shouldShowSelectionToolbar = kind == null ||
        kind == PointerDeviceKind.touch ||
        kind == PointerDeviceKind.stylus;
    final isShiftPressedValid =
        _isShiftPressed && renderEditor?.selection.baseOffset != null;
    switch (defaultTargetPlatform) {
      case TargetPlatform.android:
      case TargetPlatform.fuchsia:
        editor?.hideToolbar(false);
      case TargetPlatform.iOS:
        // On mobile platforms the selection is set on tap up.
        break;
      case TargetPlatform.macOS:
        editor?.hideToolbar();
        // On macOS, a shift-tapped unfocused field expands from 0, not from the
        // previous selection.
        if (isShiftPressedValid) {
          final fromSelection = renderEditor?.hasFocus == true
              ? null
              : const TextSelection.collapsed(offset: 0);
          _expandSelection(
              details.globalPosition, SelectionChangedCause.tap, fromSelection);
          return;
        }
        renderEditor?.selectPosition(cause: SelectionChangedCause.tap);
      case TargetPlatform.linux:
      case TargetPlatform.windows:
        editor?.hideToolbar();
        if (isShiftPressedValid) {
          _extendSelection(details.globalPosition, SelectionChangedCause.tap);
          return;
        }
        renderEditor?.selectPosition(cause: SelectionChangedCause.tap);
    }
  }

  /// Handler for [EditorTextSelectionGestureDetector.onForcePressStart].
  ///
  /// By default, it selects the word at the position of the force press,
  /// if selection is enabled.
  ///
  /// This callback is only applicable when force press is enabled.
  ///
  /// See also:
  ///
  ///  * [EditorTextSelectionGestureDetector.onForcePressStart],
  ///  which triggers this callback.
  @protected
  void onForcePressStart(ForcePressDetails details) {
    assert(delegate.forcePressEnabled);
    shouldShowSelectionToolbar = true;
    if (delegate.selectionEnabled) {
      renderEditor!.selectWordsInRange(
        details.globalPosition,
        null,
        SelectionChangedCause.forcePress,
      );
    }
  }

  /// Handler for [EditorTextSelectionGestureDetector.onForcePressEnd].
  ///
  /// By default, it selects words in the range specified in [details] and shows
  /// toolbar if it is necessary.
  ///
  /// This callback is only applicable when force press is enabled.
  ///
  /// See also:
  ///
  ///  * [EditorTextSelectionGestureDetector.onForcePressEnd],
  ///  which triggers this callback.
  @protected
  void onForcePressEnd(ForcePressDetails details) {
    assert(delegate.forcePressEnabled);
    renderEditor!.selectWordsInRange(
      details.globalPosition,
      null,
      SelectionChangedCause.forcePress,
    );
    if (checkSelectionToolbarShouldShow(isAdditionalAction: false)) {
      editor!.showToolbar();
    }
  }

  /// Whether the provided [onUserTap] callback should be dispatched on every
  /// tap or only non-consecutive taps.
  ///
  /// Defaults to false.
  @protected
  bool get onUserTapAlwaysCalled => false;

  /// Handler for [TextSelectionGestureDetector.onUserTap].
  ///
  /// By default, it serves as placeholder to enable subclass override.
  ///
  /// See also:
  ///
  ///  * [TextSelectionGestureDetector.onUserTap], which triggers this
  ///    callback.
  ///  * [TextSelectionGestureDetector.onUserTapAlwaysCalled], which controls
  ///     whether this callback is called only on the first tap in a series
  ///     of taps.
  @protected
  void onUserTap() {/* Subclass should override this method if needed. */}

  /// Handler for [EditorTextSelectionGestureDetector.onSingleTapUp].
  ///
  /// By default, it selects word edge if selection is enabled.
  ///
  /// See also:
  ///
  ///  * [EditorTextSelectionGestureDetector.onSingleTapUp], which triggers
  ///    this callback.
  @protected
  void onSingleTapUp(TapDragUpDetails details) {
    if (delegate.selectionEnabled) {
      renderEditor!.selectWordEdge(SelectionChangedCause.tap);
    }
  }

  /// onSingleTapUp for mouse right click
  @protected
  void onSecondarySingleTapUp(TapUpDetails details) {
    // added to show toolbar by right click
    if (checkSelectionToolbarShouldShow(isAdditionalAction: true)) {
      editor!.showToolbar();
    }
  }

  /// Handler for [EditorTextSelectionGestureDetector.onSingleTapCancel].
  ///
  /// By default, it services as place holder to enable subclass override.
  ///
  /// See also:
  ///
  ///  * [EditorTextSelectionGestureDetector.onSingleTapCancel], which triggers
  ///    this callback.
  @protected
  void onSingleTapCancel() {
    /* Subclass should override this method if needed. */
  }

  /// Handler for [EditorTextSelectionGestureDetector.onSingleLongTapStart].
  ///
  /// By default, it selects text position specified in [details] if selection
  /// is enabled.
  ///
  /// See also:
  ///
  ///  * [EditorTextSelectionGestureDetector.onSingleLongTapStart],
  ///  which triggers this callback.
  @protected
  void onSingleLongTapStart(LongPressStartDetails details) {
    if (delegate.selectionEnabled) {
      renderEditor!.selectPositionAt(
        from: details.globalPosition,
        cause: SelectionChangedCause.longPress,
      );
    }
  }

  /// Handler for [EditorTextSelectionGestureDetector.onSingleLongTapMoveUpdate]
  ///
  /// By default, it updates the selection location specified in [details] if
  /// selection is enabled.
  ///
  /// See also:
  ///
  ///  * [EditorTextSelectionGestureDetector.onSingleLongTapMoveUpdate], which
  ///    triggers this callback.
  @protected
  void onSingleLongTapMoveUpdate(LongPressMoveUpdateDetails details) {
    if (delegate.selectionEnabled) {
      renderEditor!.selectPositionAt(
        from: details.globalPosition,
        cause: SelectionChangedCause.longPress,
      );
    }
  }

  /// Handler for [EditorTextSelectionGestureDetector.onSingleLongTapEnd].
  ///
  /// By default, it shows toolbar if necessary.
  ///
  /// See also:
  ///
  ///  * [EditorTextSelectionGestureDetector.onSingleLongTapEnd],
  ///  which triggers this callback.
  @protected
  void onSingleLongTapEnd(LongPressEndDetails details) {
    if (checkSelectionToolbarShouldShow(isAdditionalAction: false)) {
      editor!.showToolbar();
    }
    // Q: why ?
    // A: cannot access QuillRawEditorState.updateFloatingCursor
    //
    // if (defaultTargetPlatform == TargetPlatform.iOS &&
    //     delegate.selectionEnabled &&
    //     editor?.textEditingValue.selection.isCollapsed == true) {
    //   // Update the floating cursor.
    //   final cursorPoint =
    //       RawFloatingCursorPoint(state: FloatingCursorDragState.End);
    //   //  !.updateFloatingCursor(cursorPoint);
    //   (editor as QuillRawEditorState?)?.updateFloatingCursor(cursorPoint);
    // }
  }

  /// Handler for [TextSelectionGestureDetector.onSecondaryTap].
  ///
  /// By default, selects the word if possible and shows the toolbar.
  @protected
  void onSecondaryTap() {
    if (!delegate.selectionEnabled) {
      return;
    }
    switch (defaultTargetPlatform) {
      case TargetPlatform.iOS:
      case TargetPlatform.macOS:
        if (!_lastSecondaryTapWasOnSelection ||
            renderEditor?.hasFocus == false) {
          renderEditor?.selectWord(SelectionChangedCause.tap);
        }
        if (shouldShowSelectionToolbar) {
          editor?.hideToolbar();
          editor?.showToolbar();
        }
      case TargetPlatform.android:
        if (!_lastSecondaryTapWasOnSelection ||
            renderEditor?.hasFocus == false) {
          renderEditor?.selectWord(SelectionChangedCause.tap);
        }
        if (shouldShowSelectionToolbar) {
          editor?.hideToolbar();
          editor?.showToolbar();
        }
      case TargetPlatform.fuchsia:
      case TargetPlatform.linux:
      case TargetPlatform.windows:
        if (renderEditor?.hasFocus == false) {
          renderEditor?.selectPosition(cause: SelectionChangedCause.tap);
        }
        editor?.toggleToolbar();
    }
  }

  /// Handler for [TextSelectionGestureDetector.onDoubleTapDown].
  ///
  /// By default, it selects a word through [RenderEditable.selectWord] if
  /// selectionEnabled and shows toolbar if necessary.
  ///
  /// See also:
  ///
  ///  * [TextSelectionGestureDetector.onDoubleTapDown], which triggers this
  ///    callback.
  @protected
  void onSecondaryTapDown(TapDownDetails details) {
    renderEditor?.handleSecondaryTapDown(
        TapDownDetails(globalPosition: details.globalPosition));
    shouldShowSelectionToolbar = true;
  }

  /// Handler for [EditorTextSelectionGestureDetector.onDoubleTapDown].
  ///
  /// By default, it selects a word through [RenderEditable.selectWord] if
  /// selectionEnabled and shows toolbar if necessary.
  ///
  /// See also:
  ///
  ///  * [EditorTextSelectionGestureDetector.onDoubleTapDown],
  ///  which triggers this callback.
  @protected
  void onDoubleTapDown(TapDragDownDetails details) {
    if (delegate.selectionEnabled) {
      renderEditor!.selectWord(SelectionChangedCause.tap);
      // allow the selection to get updated before trying to bring up
      // toolbars.
      //
      // if double tap happens on an editor that doesn't
      // have focus, selection hasn't been set when the toolbars
      // get added
      SchedulerBinding.instance.addPostFrameCallback((_) {
        if (checkSelectionToolbarShouldShow(isAdditionalAction: false)) {
          editor!.showToolbar();
        }
      });
    }
  }

  // Selects the set of paragraphs in a document that intersect a given range of
  // global positions.
  void _selectParagraphsInRange(
      {required Offset from, Offset? to, SelectionChangedCause? cause}) {
    final TextBoundary paragraphBoundary =
        ParagraphBoundary(editor!.textEditingValue.text);
    _selectTextBoundariesInRange(
        boundary: paragraphBoundary, from: from, to: to, cause: cause);
  }

  // Selects the set of lines in a document that intersect a given range of
  // global positions.
  void _selectLinesInRange(
      {required Offset from, Offset? to, SelectionChangedCause? cause}) {
    final TextBoundary lineBoundary = LineBoundary(renderEditor!);
    _selectTextBoundariesInRange(
        boundary: lineBoundary, from: from, to: to, cause: cause);
  }

  // Returns the location of a text boundary at `extent`. When `extent` is at
  // the end of the text, returns the previous text boundary's location.
  TextRange _moveToTextBoundary(
      TextPosition extent, TextBoundary textBoundary) {
    assert(extent.offset >= 0);
    final start = textBoundary.getLeadingTextBoundaryAt(
            extent.offset == editor!.textEditingValue.text.length
                ? extent.offset - 1
                : extent.offset) ??
        0;
    final end = textBoundary.getTrailingTextBoundaryAt(extent.offset) ??
        editor!.textEditingValue.text.length;
    return TextRange(start: start, end: end);
  }

  // Selects the set of text boundaries in a document that intersect a given
  // range of global positions.
  //
  // The set of text boundaries selected are not strictly bounded by the range
  // of global positions.
  //
  // The first and last endpoints of the selection will always be at the
  // beginning and end of a text boundary respectively.
  void _selectTextBoundariesInRange(
      {required TextBoundary boundary,
      required Offset from,
      Offset? to,
      SelectionChangedCause? cause}) {
    final fromPosition = renderEditor!.getPositionForOffset(from);
    final fromRange = _moveToTextBoundary(fromPosition, boundary);
    final toPosition =
        to == null ? fromPosition : renderEditor!.getPositionForOffset(to);
    final toRange = toPosition == fromPosition
        ? fromRange
        : _moveToTextBoundary(toPosition, boundary);
    final isFromBoundaryBeforeToBoundary = fromRange.start < toRange.end;

    final newSelection = isFromBoundaryBeforeToBoundary
        ? TextSelection(baseOffset: fromRange.start, extentOffset: toRange.end)
        : TextSelection(baseOffset: fromRange.end, extentOffset: toRange.start);

    editor!.userUpdateTextEditingValue(
        editor!.textEditingValue.copyWith(selection: newSelection),
        cause ?? SelectionChangedCause.drag);
  }

  /// Handler for [TextSelectionGestureDetector.onTripleTapDown].
  ///
  /// By default, it selects a paragraph if
  /// [TextSelectionGestureDetectorBuilderDelegate.selectionEnabled] is true
  /// and shows the toolbar if necessary.
  ///
  /// See also:
  ///
  ///  * [TextSelectionGestureDetector.onTripleTapDown], which triggers this
  ///    callback.
  @protected
  void onTripleTapDown(TapDragDownDetails details) {
    if (!delegate.selectionEnabled) {
      return;
    }

    switch (defaultTargetPlatform) {
      case TargetPlatform.android:
      case TargetPlatform.fuchsia:
      case TargetPlatform.iOS:
      case TargetPlatform.macOS:
      case TargetPlatform.windows:
        _selectParagraphsInRange(
            from: details.globalPosition, cause: SelectionChangedCause.tap);
      case TargetPlatform.linux:
        _selectLinesInRange(
            from: details.globalPosition, cause: SelectionChangedCause.tap);
    }

    if (shouldShowSelectionToolbar) {
      editor?.showToolbar();
    }
  }

  /// Handler for [EditorTextSelectionGestureDetector.onDragSelectionStart].
  ///
  /// By default, it selects a text position specified in [details].
  ///
  /// See also:
  ///
  ///  * [EditorTextSelectionGestureDetector.onDragSelectionStart],
  ///  which triggers this callback.
  @protected
  void onDragSelectionStart(TapDragStartDetails details) {
    if (delegate.selectionEnabled == false) return;
    // underline show open on ios and android,
    // when has isCollapsed, show not reposonse to tapdarg gesture
    // so that will not change texteditingvalue,
    // and same issue to TextField, tap selection area, will lost selection,
    // if (editor?.textEditingValue.selection.isCollapsed == false) return;

    final kind = details.kind;
    shouldShowSelectionToolbar = kind == null ||
        kind == PointerDeviceKind.touch ||
        kind == PointerDeviceKind.stylus;
    _dragStartSelection = renderEditor?.selection;
    _dragStartScrollOffset = _scrollPosition;
    _dragStartViewportOffset = renderEditor?.offset?.pixels ?? 0.0;
    _dragBeganOnPreviousSelection =
        _positionOnSelection(details.globalPosition, _dragStartSelection);
    if (EditorTextSelectionGestureDetector.getEffectiveConsecutiveTapCount(
            details.consecutiveTapCount) >
        1) {
      // Do not set the selection on a consecutive tap and drag.
      return;
    }

    if (_isShiftPressed &&
        renderEditor?.selection != null &&
        renderEditor?.selection.isValid == true) {
      switch (defaultTargetPlatform) {
        case TargetPlatform.iOS:
        case TargetPlatform.macOS:
          renderEditor?.extendSelection(details.globalPosition,
              cause: SelectionChangedCause.drag);

        case TargetPlatform.android:
        case TargetPlatform.fuchsia:
        case TargetPlatform.linux:
        case TargetPlatform.windows:
          renderEditor?.extendSelection(details.globalPosition,
              cause: SelectionChangedCause.drag);
      }
    } else {
      switch (defaultTargetPlatform) {
        case TargetPlatform.iOS:
          switch (details.kind) {
            case PointerDeviceKind.mouse:
            case PointerDeviceKind.trackpad:
              renderEditor?.selectPositionAt(
                from: details.globalPosition,
                cause: SelectionChangedCause.drag,
              );
            case PointerDeviceKind.stylus:
            case PointerDeviceKind.invertedStylus:
            case PointerDeviceKind.touch:
            case PointerDeviceKind.unknown:
              // For iOS platforms, a touch drag does not initiate unless the
              // editable has focus and the drag began on the previous selection.
              assert(_dragBeganOnPreviousSelection != null);
              if (renderEditor?.hasFocus == true &&
                  _dragBeganOnPreviousSelection!) {
                renderEditor?.selectPositionAt(
                  from: details.globalPosition,
                  cause: SelectionChangedCause.drag,
                );
                editor?.showMagnifier(details.globalPosition);
              }
            case null:
          }
        case TargetPlatform.android:
        case TargetPlatform.fuchsia:
          switch (details.kind) {
            case PointerDeviceKind.mouse:
            case PointerDeviceKind.trackpad:
              renderEditor?.selectPositionAt(
                from: details.globalPosition,
                cause: SelectionChangedCause.drag,
              );
            case PointerDeviceKind.stylus:
            case PointerDeviceKind.invertedStylus:
            case PointerDeviceKind.touch:
            case PointerDeviceKind.unknown:
              // For Android, Fucshia, and iOS platforms, a touch drag
              // does not initiate unless the editable has focus.
              if (renderEditor?.hasFocus == true) {
                renderEditor?.selectPositionAt(
                  from: details.globalPosition,
                  cause: SelectionChangedCause.drag,
                );
                editor?.showMagnifier(details.globalPosition);
              }
            case null:
          }
        case TargetPlatform.linux:
        case TargetPlatform.macOS:
        case TargetPlatform.windows:
          renderEditor?.selectPositionAt(
            from: details.globalPosition,
            cause: SelectionChangedCause.drag,
          );
      }
    }
  }

  /// Handler for [EditorTextSelectionGestureDetector.onDragSelectionUpdate].
  ///
  /// By default, it updates the selection location specified in the provided
  /// details objects.
  ///
  /// See also:
  ///
  ///  * [EditorTextSelectionGestureDetector.onDragSelectionUpdate],
  ///  which triggers this callback./lib/src/material/text_field.dart
  @protected
  void onDragSelectionUpdate(TapDragUpdateDetails updateDetails) {
    if (delegate.selectionEnabled == false) return;
    // if (editor?.textEditingValue.selection.isCollapsed == false) return;
    if (!_isShiftPressed) {
      // Adjust the drag start offset for possible viewport offset changes.
      final editableOffset = Offset(
          0, (renderEditor!.offset?.pixels ?? 0) - _dragStartViewportOffset);
      final scrollableOffset =
          Offset(0, _scrollPosition - _dragStartScrollOffset);
      final dragStartGlobalPosition =
          updateDetails.globalPosition - updateDetails.offsetFromOrigin;

      // Select word by word.
      if (EditorTextSelectionGestureDetector.getEffectiveConsecutiveTapCount(
              updateDetails.consecutiveTapCount) ==
          2) {
        renderEditor?.selectWordsInRange(
          dragStartGlobalPosition - editableOffset - scrollableOffset,
          updateDetails.globalPosition,
          SelectionChangedCause.drag,
        );

        switch (updateDetails.kind) {
          case PointerDeviceKind.stylus:
          case PointerDeviceKind.invertedStylus:
          case PointerDeviceKind.touch:
          case PointerDeviceKind.unknown:
            return editor?.updateMagnifier(updateDetails.globalPosition);
          case PointerDeviceKind.mouse:
          case PointerDeviceKind.trackpad:
          case null:
            return;
        }
      }

      // Select paragraph-by-paragraph.
      if (EditorTextSelectionGestureDetector.getEffectiveConsecutiveTapCount(
              updateDetails.consecutiveTapCount) ==
          3) {
        switch (defaultTargetPlatform) {
          case TargetPlatform.android:
          case TargetPlatform.fuchsia:
          case TargetPlatform.iOS:
            switch (updateDetails.kind) {
              case PointerDeviceKind.mouse:
              case PointerDeviceKind.trackpad:
                return _selectParagraphsInRange(
                  from: dragStartGlobalPosition -
                      editableOffset -
                      scrollableOffset,
                  to: updateDetails.globalPosition,
                  cause: SelectionChangedCause.drag,
                );
              case PointerDeviceKind.stylus:
              case PointerDeviceKind.invertedStylus:
              case PointerDeviceKind.touch:
              case PointerDeviceKind.unknown:
              case null:
                // Triple tap to drag is not present on these platforms when using
                // non-precise pointer devices at the moment.
                break;
            }
            return;
          case TargetPlatform.linux:
            return _selectLinesInRange(
              from: dragStartGlobalPosition - editableOffset - scrollableOffset,
              to: updateDetails.globalPosition,
              cause: SelectionChangedCause.drag,
            );
          case TargetPlatform.windows:
          case TargetPlatform.macOS:
            return _selectParagraphsInRange(
              from: dragStartGlobalPosition - editableOffset - scrollableOffset,
              to: updateDetails.globalPosition,
              cause: SelectionChangedCause.drag,
            );
        }
      }

      switch (defaultTargetPlatform) {
        case TargetPlatform.iOS:
          // With a touch device, nothing should happen, unless there was a double tap, or
          // there was a collapsed selection, and the tap/drag position is at the collapsed selection.
          // In that case the caret should move with the drag position.
          //
          // With a mouse device, a drag should select the range from the origin of the drag
          // to the current position of the drag.
          switch (updateDetails.kind) {
            case PointerDeviceKind.mouse:
            case PointerDeviceKind.trackpad:
              renderEditor?.selectPositionAt(
                from:
                    dragStartGlobalPosition - editableOffset - scrollableOffset,
                to: updateDetails.globalPosition,
                cause: SelectionChangedCause.drag,
              );
              return;
            case PointerDeviceKind.stylus:
            case PointerDeviceKind.invertedStylus:
            case PointerDeviceKind.touch:
            case PointerDeviceKind.unknown:
              assert(_dragBeganOnPreviousSelection != null);
              if (renderEditor?.hasFocus == true &&
                  _dragStartSelection!.isCollapsed &&
                  _dragBeganOnPreviousSelection!) {
                renderEditor?.selectPositionAt(
                  from: updateDetails.globalPosition,
                  cause: SelectionChangedCause.drag,
                );
                return editor?.updateMagnifier(updateDetails.globalPosition);
              }
            case null:
              break;
          }
          return;
        case TargetPlatform.android:
        case TargetPlatform.fuchsia:
          // With a precise pointer device, such as a mouse, trackpad, or stylus,
          // the drag will select the text spanning the origin of the drag to the end of the drag.
          // With a touch device, the cursor should move with the drag.
          switch (updateDetails.kind) {
            case PointerDeviceKind.mouse:
            case PointerDeviceKind.trackpad:
            case PointerDeviceKind.stylus:
            case PointerDeviceKind.invertedStylus:
              renderEditor?.selectPositionAt(
                from:
                    dragStartGlobalPosition - editableOffset - scrollableOffset,
                to: updateDetails.globalPosition,
                cause: SelectionChangedCause.drag,
              );
              return;
            case PointerDeviceKind.touch:
            case PointerDeviceKind.unknown:
              if (renderEditor?.hasFocus == true) {
                renderEditor?.selectPositionAt(
                  from: updateDetails.globalPosition,
                  cause: SelectionChangedCause.drag,
                );
                return editor?.updateMagnifier(updateDetails.globalPosition);
              }
            case null:
              break;
          }
          return;
        case TargetPlatform.macOS:
        case TargetPlatform.linux:
        case TargetPlatform.windows:
          renderEditor?.selectPositionAt(
            from: dragStartGlobalPosition - editableOffset - scrollableOffset,
            to: updateDetails.globalPosition,
            cause: SelectionChangedCause.drag,
          );
      }
    }

    if (_dragStartSelection!.isCollapsed ||
        (defaultTargetPlatform != TargetPlatform.iOS &&
            defaultTargetPlatform != TargetPlatform.macOS)) {
      return _extendSelection(
          updateDetails.globalPosition, SelectionChangedCause.drag);
    }

    // If the drag inverts the selection, Mac and iOS revert to the initial
    // selection.
    final selection = renderEditor!.selection;
    final nextExtent =
        renderEditor!.getPositionForOffset(updateDetails.globalPosition);

    final isShiftTapDragSelectionForward =
        _dragStartSelection!.baseOffset < _dragStartSelection!.extentOffset;
    final isInverted = isShiftTapDragSelectionForward
        ? nextExtent.offset < _dragStartSelection!.baseOffset
        : nextExtent.offset > _dragStartSelection!.baseOffset;
    if (isInverted && selection.baseOffset == _dragStartSelection!.baseOffset) {
      editor?.userUpdateTextEditingValue(
        editor!.textEditingValue.copyWith(
          selection: TextSelection(
            baseOffset: _dragStartSelection!.extentOffset,
            extentOffset: nextExtent.offset,
          ),
        ),
        SelectionChangedCause.drag,
      );
    } else if (!isInverted &&
        nextExtent.offset != _dragStartSelection!.baseOffset &&
        selection.baseOffset != _dragStartSelection!.baseOffset) {
      editor?.userUpdateTextEditingValue(
        editor!.textEditingValue.copyWith(
          selection: TextSelection(
            baseOffset: _dragStartSelection!.baseOffset,
            extentOffset: nextExtent.offset,
          ),
        ),
        SelectionChangedCause.drag,
      );
    } else {
      _extendSelection(
          updateDetails.globalPosition, SelectionChangedCause.drag);
    }
  }

  /// Handler for [EditorTextSelectionGestureDetector.onDragSelectionEnd].
  ///
  /// By default, it services as place holder to enable subclass override.
  ///
  /// See also:
  ///
  ///  * [EditorTextSelectionGestureDetector.onDragSelectionEnd],
  ///  which triggers this callback.
  @protected
  void onDragSelectionEnd(TapDragEndDetails details) {
    // if (editor?.textEditingValue.selection.isCollapsed == false) return;
    renderEditor!.handleDragEnd(details);
    if (isDesktop(supportWeb: true) &&
        delegate.selectionEnabled &&
        checkSelectionToolbarShouldShow(isAdditionalAction: false)) {
      // added to show selection copy/paste toolbar after drag to select
      editor!.showToolbar();
    }
    editor?.hideMagnifier();
  }

  /// Returns a [EditorTextSelectionGestureDetector] configured with
  /// the handlers provided by this builder.
  ///
  /// The [child] or its subtree should contain [EditableText].
  Widget build({
    required HitTestBehavior behavior,
    required Widget child,
    Key? key,
    bool detectWordBoundary = true,
  }) {
    return EditorTextSelectionGestureDetector(
      key: key,
      onTapTrackStart: onTapTrackStart,
      onTapTrackReset: onTapTrackReset,
      onTapDown: onTapDown,
      onForcePressStart: delegate.forcePressEnabled ? onForcePressStart : null,
      onForcePressEnd: delegate.forcePressEnabled ? onForcePressEnd : null,
      onSecondaryTap: onSecondaryTap,
      onSecondaryTapDown: onSecondaryTapDown,
      onSingleTapUp: onSingleTapUp,
      onSingleTapCancel: onSingleTapCancel,
      onUserTap: onUserTap,
      onSingleLongTapStart: onSingleLongTapStart,
      onSingleLongTapMoveUpdate: onSingleLongTapMoveUpdate,
      onSingleLongTapEnd: onSingleLongTapEnd,
      onDoubleTapDown: onDoubleTapDown,
      onTripleTapDown: onTripleTapDown,
      onDragSelectionStart: onDragSelectionStart,
      onDragSelectionUpdate: onDragSelectionUpdate,
      onDragSelectionEnd: onDragSelectionEnd,
      onUserTapAlwaysCalled: onUserTapAlwaysCalled,
      behavior: behavior,
      child: child,
    );
  }
}
jonasbernardo commented 2 weeks ago

I fixed it, in the delegate.dart class

jonasbernardo commented 2 weeks ago

in the method onsecondaryTap I replicated from macos to android

jonasbernardo commented 2 weeks ago
case TargetPlatform.android:
  if (!_lastSecondaryTapWasOnSelection ||
     renderEditor?.hasFocus == false) {
          renderEditor?.selectWord(SelectionChangedCause.tap);
  }
singerdmx commented 1 week ago

Maybe create a PR for this? Should it be restricted to Android platform only?

singerdmx commented 1 week ago

@AtlasAutocode is this fixed by your PR?

AtlasAutocode commented 1 week ago

@AtlasAutocode is this fixed by your PR?

@singerdmx - No, this is a different matter - I don't know why #2173 referenced this issue. The affected file is used to select appropriate platform-specific actions so it will automatically be Android-only.

@jonasbernardo - can you submit a Pull Request for your fix so others can benefit? If you can't, please let me know and I will take a look at it.

AtlasAutocode commented 1 week ago

@jonasbernardo I am unable to reproduce this issue or your fix. Right click on an android emulator does not trigger onSecondaryTap - is there some configuration option that I am missing?

AtlasAutocode commented 6 days ago

@singerdmx we have not gotten a response from the OP, and I cannot confirm that their fix works. Do you recommend we close this issue? Or implement their fix and hope for the best?

I think right clicking with a mouse on an android device would be a very rare event, but I am not an android expert.

For background: I suspect OP has moved to their own branch because of #2085 and is no longer using the shared repository.

CatHood0 commented 6 days ago

I suspect OP has moved to their own branch because of https://github.com/singerdmx/flutter-quill/issues/2085 and is no longer using the shared repository.

This is not related, but I want to comment that I tried to solve this problem, and it is not that simple. It seems that it is more of a rendering problem. If we go to text_line there is a part that renders the leading (in this case the checkbox), however, this, regardless of the offset you pass, will always put the box where you want, BUT, the InkWell that is the one that lets us do the check or uncheck action, always stays at the same offset no matter what is done or moved.