Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SuperTextField] Add ability to override tap gestures (Resolves #2447) #2455

Merged
merged 6 commits into from
Jan 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading