Skip to content

Commit

Permalink
[SuperTextField] Add ability to override tap gestures (Resolves #2447) (
Browse files Browse the repository at this point in the history
  • Loading branch information
angelosilvestre authored and web-flow committed Jan 3, 2025
1 parent 1f08366 commit d21b620
Show file tree
Hide file tree
Showing 13 changed files with 1,266 additions and 56 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -43,19 +43,20 @@ class _InteractiveTextFieldDemoState extends State<InteractiveTextFieldDemo> {
super.dispose();
}

void _onRightClick(
BuildContext textFieldContext, AttributedTextEditingController textController, Offset localOffset) {
TapHandlingInstruction _onRightClick(SuperTextFieldGestureDetails details) {
// Only show menu if some text is selected
if (textController.selection.isCollapsed) {
return;
if (details.textController.selection.isCollapsed) {
return TapHandlingInstruction.continueHandling;
}

final overlay = Overlay.of(context);
final overlayBox = overlay.context.findRenderObject() as RenderBox?;
final textFieldBox = textFieldContext.findRenderObject() as RenderBox;
_popupOffset = textFieldBox.localToGlobal(localOffset, ancestor: overlayBox);
final overlayBox = overlay.context.findRenderObject() as RenderBox;

_popupOffset = overlayBox.globalToLocal(details.globalOffset);

_popupOverlayController.show();

return TapHandlingInstruction.halt;
}

void _closePopup() {
Expand Down Expand Up @@ -86,6 +87,9 @@ class _InteractiveTextFieldDemoState extends State<InteractiveTextFieldDemo> {
textStyleBuilder: demoTextStyleBuilder,
blinkTimingMode: BlinkTimingMode.timer,
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
tapHandlers: [
_SuperTextFieldRightClickListener(rightClickHandler: _onRightClick),
],
decorationBuilder: (context, child) {
return Container(
decoration: BoxDecoration(
Expand All @@ -109,7 +113,6 @@ class _InteractiveTextFieldDemoState extends State<InteractiveTextFieldDemo> {
hintBehavior: HintBehavior.displayHintUntilTextEntered,
minLines: 5,
maxLines: 5,
onRightClick: _onRightClick,
),
),
),
Expand Down Expand Up @@ -168,3 +171,20 @@ class _InteractiveTextFieldDemoState extends State<InteractiveTextFieldDemo> {
);
}
}

/// A [SuperTextFieldTapHandler] that listens for right clicks and invokes the
/// [rightClickHandler] when a right click happens.
class _SuperTextFieldRightClickListener extends SuperTextFieldTapHandler {
_SuperTextFieldRightClickListener({
required this.rightClickHandler,
});

final RightClickHandler rightClickHandler;

@override
TapHandlingInstruction onSecondaryTapUp(SuperTextFieldGestureDetails details) {
return rightClickHandler(details);
}
}

typedef RightClickHandler = TapHandlingInstruction Function(SuperTextFieldGestureDetails details);
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,15 @@ import 'package:super_editor/src/core/document_layout.dart';
/// Delegate for mouse status and clicking on special types of content,
/// e.g., tapping on a link open the URL.
///
/// Listeners are notified when any time that the desired mouse cursor
/// may have changed.
/// Each [ContentTapDelegate] notifies its listeners whenever an
/// internal policy changes, which might impact the mouse cursor
/// style. For example, a handler in a desktop app, when hovering
/// over a link, might initially show a text cursor, but when the
/// user pressed CMD (or CTL), the mouse cursor would change to a
/// click cursor. Only the individual handlers know when or if such
/// a change should occur. When such a change does occur, the
/// handler notifies its listeners, and the handler expects that
/// someone will ask it for the desired mouse cursor style.
abstract class ContentTapDelegate with ChangeNotifier {
MouseCursor? mouseCursorForContentHover(DocumentPosition hoverPosition) {
return null;
Expand Down
158 changes: 158 additions & 0 deletions super_editor/lib/src/super_textfield/android/_user_interaction.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:super_editor/src/infrastructure/_logging.dart';
import 'package:super_editor/src/infrastructure/document_gestures_interaction_overrides.dart';
import 'package:super_editor/src/infrastructure/flutter/flutter_scheduler.dart';
import 'package:super_editor/src/infrastructure/multi_tap_gesture.dart';
import 'package:super_editor/src/super_textfield/infrastructure/text_field_gestures_interaction_overrides.dart';
import 'package:super_editor/src/super_textfield/super_textfield.dart';
import 'package:super_text_layout/super_text_layout.dart';

Expand Down Expand Up @@ -51,6 +53,7 @@ class AndroidTextFieldTouchInteractor extends StatefulWidget {
required this.getGlobalCaretRect,
required this.isMultiline,
required this.handleColor,
this.tapHandlers = const [],
this.showDebugPaint = false,
required this.child,
}) : super(key: key);
Expand Down Expand Up @@ -93,6 +96,9 @@ class AndroidTextFieldTouchInteractor extends StatefulWidget {
/// The color of expanded selection drag handles.
final Color handleColor;

/// {@macro super_text_field_tap_handlers}
final List<SuperTextFieldTapHandler> tapHandlers;

/// Whether to paint debugging guides and regions.
final bool showDebugPaint;

Expand Down Expand Up @@ -166,9 +172,47 @@ class AndroidTextFieldTouchInteractorState extends State<AndroidTextFieldTouchIn
}
}

void _onTapDown(TapDownDetails details) {
final textOffset = _globalOffsetToTextOffset(details.globalPosition);

for (final handler in widget.tapHandlers) {
final result = handler.onTapDown(
SuperTextFieldGestureDetails(
textLayout: _textLayout,
textController: widget.textController,
globalOffset: details.globalPosition,
layoutOffset: details.localPosition,
textOffset: textOffset,
),
);

if (result == TapHandlingInstruction.halt) {
return;
}
}
}

void _onTapUp(TapUpDetails details) {
_log.fine('User released a tap');

final textOffset = _globalOffsetToTextOffset(details.globalPosition);

for (final handler in widget.tapHandlers) {
final result = handler.onTapUp(
SuperTextFieldGestureDetails(
textLayout: _textLayout,
textController: widget.textController,
globalOffset: details.globalPosition,
layoutOffset: details.localPosition,
textOffset: textOffset,
),
);

if (result == TapHandlingInstruction.halt) {
return;
}
}

if (widget.focusNode.hasFocus && widget.textController.isAttachedToIme) {
widget.textController.showKeyboard();
} else {
Expand Down Expand Up @@ -209,6 +253,16 @@ class AndroidTextFieldTouchInteractorState extends State<AndroidTextFieldTouchIn
..startCollapsedHandleAutoHideCountdown();
}

void _onTapCancel() {
for (final handler in widget.tapHandlers) {
final result = handler.onTapCancel();

if (result == TapHandlingInstruction.halt) {
return;
}
}
}

/// Places the caret in the field's text based on the given [localOffset],
/// and displays the drag handle.
void _selectAtOffset(Offset localOffset) {
Expand Down Expand Up @@ -242,6 +296,25 @@ class AndroidTextFieldTouchInteractorState extends State<AndroidTextFieldTouchIn

void _onDoubleTapDown(TapDownDetails details) {
_log.fine("User double-tapped down");

final textOffset = _globalOffsetToTextOffset(details.globalPosition);

for (final handler in widget.tapHandlers) {
final result = handler.onDoubleTapDown(
SuperTextFieldGestureDetails(
textLayout: _textLayout,
textController: widget.textController,
globalOffset: details.globalPosition,
layoutOffset: details.localPosition,
textOffset: textOffset,
),
);

if (result == TapHandlingInstruction.halt) {
return;
}
}

widget.focusNode.requestFocus();

final tapTextPosition = _getTextPositionAtOffset(details.localPosition);
Expand All @@ -265,8 +338,57 @@ class AndroidTextFieldTouchInteractorState extends State<AndroidTextFieldTouchIn
}
}

void _onDoubleTapUp(TapUpDetails details) {
final textOffset = _globalOffsetToTextOffset(details.globalPosition);

for (final handler in widget.tapHandlers) {
final result = handler.onDoubleTapUp(
SuperTextFieldGestureDetails(
textLayout: _textLayout,
textController: widget.textController,
globalOffset: details.globalPosition,
layoutOffset: details.localPosition,
textOffset: textOffset,
),
);

if (result == TapHandlingInstruction.halt) {
return;
}
}
}

void _onDoubleTapCancel() {
for (final handler in widget.tapHandlers) {
final result = handler.onDoubleTapCancel();

if (result == TapHandlingInstruction.halt) {
return;
}
}
}

void _onTripleTapDown(TapDownDetails details) {
_log.fine("User triple-tapped down");

final textOffset = _globalOffsetToTextOffset(details.globalPosition);

for (final handler in widget.tapHandlers) {
final result = handler.onTripleTapDown(
SuperTextFieldGestureDetails(
textLayout: _textLayout,
textController: widget.textController,
globalOffset: details.globalPosition,
layoutOffset: details.localPosition,
textOffset: textOffset,
),
);

if (result == TapHandlingInstruction.halt) {
return;
}
}

final tapTextPosition = _textLayout.getPositionAtOffset(details.localPosition)!;

widget.textController.selection =
Expand All @@ -282,6 +404,36 @@ class AndroidTextFieldTouchInteractorState extends State<AndroidTextFieldTouchIn
}
}

void _onTripleTapUp(TapUpDetails details) {
final textOffset = _globalOffsetToTextOffset(details.globalPosition);

for (final handler in widget.tapHandlers) {
final result = handler.onTripleTapUp(
SuperTextFieldGestureDetails(
textLayout: _textLayout,
textController: widget.textController,
globalOffset: details.globalPosition,
layoutOffset: details.localPosition,
textOffset: textOffset,
),
);

if (result == TapHandlingInstruction.halt) {
return;
}
}
}

void _onTripleTapCancel() {
for (final handler in widget.tapHandlers) {
final result = handler.onTripleTapCancel();

if (result == TapHandlingInstruction.halt) {
return;
}
}
}

void _onPanStart(DragStartDetails details) {
_log.fine("User started a pan");

Expand Down Expand Up @@ -479,9 +631,15 @@ class AndroidTextFieldTouchInteractorState extends State<AndroidTextFieldTouchIn
() => TapSequenceGestureRecognizer(),
(TapSequenceGestureRecognizer recognizer) {
recognizer
..onTapDown = _onTapDown
..onTapUp = _onTapUp
..onTapCancel = _onTapCancel
..onDoubleTapDown = _onDoubleTapDown
..onDoubleTapUp = _onDoubleTapUp
..onDoubleTapCancel = _onDoubleTapCancel
..onTripleTapDown = _onTripleTapDown
..onTripleTapUp = _onTripleTapUp
..onTripleTapCancel = _onTripleTapCancel
..gestureSettings = gestureSettings;
},
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import 'package:super_editor/src/super_textfield/android/_editing_controls.dart'
import 'package:super_editor/src/super_textfield/android/_user_interaction.dart';
import 'package:super_editor/src/super_textfield/infrastructure/fill_width_if_constrained.dart';
import 'package:super_editor/src/super_textfield/infrastructure/hint_text.dart';
import 'package:super_editor/src/super_textfield/infrastructure/text_field_gestures_interaction_overrides.dart';
import 'package:super_editor/src/super_textfield/infrastructure/text_scrollview.dart';
import 'package:super_editor/src/super_textfield/input_method_engine/_ime_text_editing_controller.dart';
import 'package:super_text_layout/super_text_layout.dart';
Expand Down Expand Up @@ -44,6 +45,7 @@ class SuperAndroidTextField extends StatefulWidget {
this.textInputAction,
this.imeConfiguration,
this.showComposingUnderline = true,
this.tapHandlers = const [],
this.popoverToolbarBuilder = _defaultAndroidToolbarBuilder,
this.showDebugPaint = false,
this.padding,
Expand Down Expand Up @@ -139,6 +141,9 @@ class SuperAndroidTextField extends StatefulWidget {
/// Whether to show an underline beneath the text in the composing region.
final bool showComposingUnderline;

/// {@macro super_text_field_tap_handlers}
final List<SuperTextFieldTapHandler> tapHandlers;

/// Whether to paint debug guides.
final bool showDebugPaint;

Expand Down Expand Up @@ -555,6 +560,7 @@ class SuperAndroidTextFieldState extends State<SuperAndroidTextField>
link: _textFieldLayerLink,
child: AndroidTextFieldTouchInteractor(
focusNode: _focusNode,
tapHandlers: widget.tapHandlers,
textKey: _textContentKey,
getGlobalCaretRect: _getGlobalCaretRect,
textFieldLayerLink: _textFieldLayerLink,
Expand Down
Loading

0 comments on commit d21b620

Please sign in to comment.