Skip to content

Commit

Permalink
[SuperEditor] Add tap handler to add an empty paragraph when tapping …
Browse files Browse the repository at this point in the history
…below the end of document (Resolves #2395) (#2397)
  • Loading branch information
angelosilvestre authored and matthew-carroll committed Nov 20, 2024
1 parent 82e0c98 commit 8ba09ff
Show file tree
Hide file tree
Showing 13 changed files with 652 additions and 233 deletions.
129 changes: 92 additions & 37 deletions super_editor/lib/src/default_editor/document_gestures_mouse.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'dart:async';

import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/services.dart';
Expand Down Expand Up @@ -49,7 +50,7 @@ class DocumentMouseInteractor extends StatefulWidget {
required this.getDocumentLayout,
required this.selectionNotifier,
required this.selectionChanges,
this.contentTapHandler,
this.contentTapHandlers,
required this.autoScroller,
required this.fillViewport,
this.showDebugPaint = false,
Expand All @@ -64,9 +65,12 @@ class DocumentMouseInteractor extends StatefulWidget {
final Stream<DocumentSelectionChange> selectionChanges;
final ValueListenable<DocumentSelection?> selectionNotifier;

/// Optional handler that responds to taps on content, e.g., opening
/// 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.
final ContentTapDelegate? contentTapHandler;
///
/// If a handler returns [TapHandlingInstruction.halt], no subsequent handlers
/// nor the default tap behavior will be executed.
final List<ContentTapDelegate>? contentTapHandlers;

/// Auto-scrolling delegate.
final AutoScrollController autoScroller;
Expand Down Expand Up @@ -124,7 +128,12 @@ class _DocumentMouseInteractorState extends State<DocumentMouseInteractor> with
widget.autoScroller
..addListener(_updateDragSelection)
..addListener(_updateMouseCursorAtLatestOffset);
widget.contentTapHandler?.addListener(_updateMouseCursorAtLatestOffset);

if (widget.contentTapHandlers != null) {
for (final handler in widget.contentTapHandlers!) {
handler.addListener(_updateMouseCursorAtLatestOffset);
}
}
}

@override
Expand All @@ -148,15 +157,28 @@ class _DocumentMouseInteractorState extends State<DocumentMouseInteractor> with
..addListener(_updateDragSelection)
..addListener(_updateMouseCursorAtLatestOffset);
}
if (widget.contentTapHandler != oldWidget.contentTapHandler) {
oldWidget.contentTapHandler?.removeListener(_updateMouseCursorAtLatestOffset);
widget.contentTapHandler?.addListener(_updateMouseCursorAtLatestOffset);
if (!const DeepCollectionEquality().equals(oldWidget.contentTapHandlers, widget.contentTapHandlers)) {
if (oldWidget.contentTapHandlers != null) {
for (final handler in oldWidget.contentTapHandlers!) {
handler.removeListener(_updateMouseCursorAtLatestOffset);
}
}

if (widget.contentTapHandlers != null) {
for (final handler in widget.contentTapHandlers!) {
handler.addListener(_updateMouseCursorAtLatestOffset);
}
}
}
}

@override
void dispose() {
widget.contentTapHandler?.removeListener(_updateMouseCursorAtLatestOffset);
if (widget.contentTapHandlers != null) {
for (final handler in widget.contentTapHandlers!) {
handler.removeListener(_updateMouseCursorAtLatestOffset);
}
}
if (widget.focusNode == null) {
_focusNode.dispose();
}
Expand Down Expand Up @@ -250,26 +272,34 @@ class _DocumentMouseInteractorState extends State<DocumentMouseInteractor> with
editorGesturesLog.info("Tap up on document");
final docOffset = _getDocOffsetFromGlobalOffset(details.globalPosition);
editorGesturesLog.fine(" - document offset: $docOffset");
final docPosition = _docLayout.getDocumentPositionNearestToOffset(docOffset);
editorGesturesLog.fine(" - tapped document position: $docPosition");

_focusNode.requestFocus();

if (widget.contentTapHandlers != null) {
for (final handler in widget.contentTapHandlers!) {
final result = handler.onTap(
DocumentTapDetails(
documentLayout: _docLayout,
layoutOffset: docOffset,
globalOffset: details.globalPosition,
),
);
if (result == TapHandlingInstruction.halt) {
// The custom tap handler doesn't want us to react at all
// to the tap.
return;
}
}
}

final docPosition = _docLayout.getDocumentPositionNearestToOffset(docOffset);
editorGesturesLog.fine(" - tapped document position: $docPosition");
if (docPosition == null) {
editorGesturesLog.fine("No document content at ${details.globalPosition}.");
_clearSelection();
return;
}

if (widget.contentTapHandler != null) {
final result = widget.contentTapHandler!.onTap(docPosition);
if (result == TapHandlingInstruction.halt) {
// The custom tap handler doesn't want us to react at all
// to the tap.
return;
}
}

final tappedComponent = _docLayout.getComponentByNodeId(docPosition.nodeId)!;
final expandSelection = _isShiftPressed && _currentSelection != null;

Expand Down Expand Up @@ -307,18 +337,26 @@ class _DocumentMouseInteractorState extends State<DocumentMouseInteractor> with
editorGesturesLog.info("Double tap down on document");
final docOffset = _getDocOffsetFromGlobalOffset(details.globalPosition);
editorGesturesLog.fine(" - document offset: $docOffset");
final docPosition = _docLayout.getDocumentPositionNearestToOffset(docOffset);
editorGesturesLog.fine(" - tapped document position: $docPosition");

if (docPosition != null && widget.contentTapHandler != null) {
final result = widget.contentTapHandler!.onDoubleTap(docPosition);
if (result == TapHandlingInstruction.halt) {
// The custom tap handler doesn't want us to react at all
// to the tap.
return;
if (widget.contentTapHandlers != null) {
for (final handler in widget.contentTapHandlers!) {
final result = handler.onDoubleTap(
DocumentTapDetails(
documentLayout: _docLayout,
layoutOffset: docOffset,
globalOffset: details.globalPosition,
),
);
if (result == TapHandlingInstruction.halt) {
// The custom tap handler doesn't want us to react at all
// to the tap.
return;
}
}
}

final docPosition = _docLayout.getDocumentPositionNearestToOffset(docOffset);
editorGesturesLog.fine(" - tapped document position: $docPosition");
if (docPosition != null) {
final tappedComponent = _docLayout.getComponentByNodeId(docPosition.nodeId)!;
if (!tappedComponent.isVisualSelectionSupported()) {
Expand Down Expand Up @@ -408,18 +446,26 @@ class _DocumentMouseInteractorState extends State<DocumentMouseInteractor> with
editorGesturesLog.info("Triple down down on document");
final docOffset = _getDocOffsetFromGlobalOffset(details.globalPosition);
editorGesturesLog.fine(" - document offset: $docOffset");
final docPosition = _docLayout.getDocumentPositionNearestToOffset(docOffset);
editorGesturesLog.fine(" - tapped document position: $docPosition");

if (docPosition != null && widget.contentTapHandler != null) {
final result = widget.contentTapHandler!.onTripleTap(docPosition);
if (result == TapHandlingInstruction.halt) {
// The custom tap handler doesn't want us to react at all
// to the tap.
return;
if (widget.contentTapHandlers != null) {
for (final handler in widget.contentTapHandlers!) {
final result = handler.onTripleTap(
DocumentTapDetails(
documentLayout: _docLayout,
layoutOffset: docOffset,
globalOffset: details.globalPosition,
),
);
if (result == TapHandlingInstruction.halt) {
// The custom tap handler doesn't want us to react at all
// to the tap.
return;
}
}
}

final docPosition = _docLayout.getDocumentPositionNearestToOffset(docOffset);
editorGesturesLog.fine(" - tapped document position: $docPosition");
if (docPosition != null) {
final tappedComponent = _docLayout.getComponentByNodeId(docPosition.nodeId)!;
if (!tappedComponent.isVisualSelectionSupported()) {
Expand Down Expand Up @@ -715,8 +761,17 @@ Updating drag selection:
return;
}

final cursorForContent = widget.contentTapHandler?.mouseCursorForContentHover(docPosition);
_mouseCursor.value = cursorForContent ?? SystemMouseCursors.text;
if (widget.contentTapHandlers != null) {
for (final handler in widget.contentTapHandlers!) {
final cursorForContent = handler.mouseCursorForContentHover(docPosition);
if (cursorForContent != null) {
_mouseCursor.value = cursorForContent;
return;
}
}
}

_mouseCursor.value = SystemMouseCursors.text;
}

@override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -405,7 +405,7 @@ class AndroidDocumentTouchInteractor extends StatefulWidget {
required this.openSoftwareKeyboard,
required this.scrollController,
required this.fillViewport,
this.contentTapHandler,
this.contentTapHandlers,
this.dragAutoScrollBoundary = const AxisOffset.symmetric(54),
required this.dragHandleAutoScroller,
this.showDebugPaint = false,
Expand All @@ -422,9 +422,12 @@ class AndroidDocumentTouchInteractor extends StatefulWidget {
/// A callback that should open the software keyboard when invoked.
final VoidCallback openSoftwareKeyboard;

/// Optional handler that responds to taps on content, e.g., opening
/// 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.
final ContentTapDelegate? contentTapHandler;
///
/// If a handler returns [TapHandlingInstruction.halt], no subsequent handlers
/// nor the default tap behavior will be executed.
final List<ContentTapDelegate>? contentTapHandlers;

final ScrollController scrollController;

Expand Down Expand Up @@ -725,18 +728,27 @@ class _AndroidDocumentTouchInteractorState extends State<AndroidDocumentTouchInt
editorGesturesLog.info("Tap down on document");
final docOffset = _getDocumentOffsetFromGlobalOffset(details.globalPosition);
editorGesturesLog.fine(" - document offset: $docOffset");
final docPosition = _docLayout.getDocumentPositionNearestToOffset(docOffset);
editorGesturesLog.fine(" - tapped document position: $docPosition");

if (widget.contentTapHandler != null && docPosition != null) {
final result = widget.contentTapHandler!.onTap(docPosition);
if (result == TapHandlingInstruction.halt) {
// The custom tap handler doesn't want us to react at all
// to the tap.
return;
if (widget.contentTapHandlers != null) {
for (final handler in widget.contentTapHandlers!) {
final result = handler.onTap(
DocumentTapDetails(
documentLayout: _docLayout,
layoutOffset: docOffset,
globalOffset: details.globalPosition,
),
);
if (result == TapHandlingInstruction.halt) {
// The custom tap handler doesn't want us to react at all
// to the tap.
return;
}
}
}

final docPosition = _docLayout.getDocumentPositionNearestToOffset(docOffset);
editorGesturesLog.fine(" - tapped document position: $docPosition");

bool didTapOnExistingSelection = false;
if (docPosition != null) {
final selection = widget.selection.value;
Expand Down Expand Up @@ -783,18 +795,27 @@ class _AndroidDocumentTouchInteractorState extends State<AndroidDocumentTouchInt
editorGesturesLog.info("Double tap down on document");
final docOffset = _getDocumentOffsetFromGlobalOffset(details.globalPosition);
editorGesturesLog.fine(" - document offset: $docOffset");
final docPosition = _docLayout.getDocumentPositionNearestToOffset(docOffset);
editorGesturesLog.fine(" - tapped document position: $docPosition");

if (docPosition != null && widget.contentTapHandler != null) {
final result = widget.contentTapHandler!.onDoubleTap(docPosition);
if (result == TapHandlingInstruction.halt) {
// The custom tap handler doesn't want us to react at all
// to the tap.
return;
if (widget.contentTapHandlers != null) {
for (final handler in widget.contentTapHandlers!) {
final result = handler.onDoubleTap(
DocumentTapDetails(
documentLayout: _docLayout,
layoutOffset: docOffset,
globalOffset: details.globalPosition,
),
);
if (result == TapHandlingInstruction.halt) {
// The custom tap handler doesn't want us to react at all
// to the tap.
return;
}
}
}

final docPosition = _docLayout.getDocumentPositionNearestToOffset(docOffset);
editorGesturesLog.fine(" - tapped document position: $docPosition");

if (docPosition != null) {
final tappedComponent = _docLayout.getComponentByNodeId(docPosition.nodeId)!;
if (!tappedComponent.isVisualSelectionSupported()) {
Expand Down Expand Up @@ -857,18 +878,26 @@ class _AndroidDocumentTouchInteractorState extends State<AndroidDocumentTouchInt
editorGesturesLog.info("Triple tap down on document");
final docOffset = _getDocumentOffsetFromGlobalOffset(details.globalPosition);
editorGesturesLog.fine(" - document offset: $docOffset");
final docPosition = _docLayout.getDocumentPositionNearestToOffset(docOffset);
editorGesturesLog.fine(" - tapped document position: $docPosition");

if (docPosition != null && widget.contentTapHandler != null) {
final result = widget.contentTapHandler!.onTripleTap(docPosition);
if (result == TapHandlingInstruction.halt) {
// The custom tap handler doesn't want us to react at all
// to the tap.
return;
if (widget.contentTapHandlers != null) {
for (final handler in widget.contentTapHandlers!) {
final result = handler.onTripleTap(
DocumentTapDetails(
documentLayout: _docLayout,
layoutOffset: docOffset,
globalOffset: details.globalPosition,
),
);
if (result == TapHandlingInstruction.halt) {
// The custom tap handler doesn't want us to react at all
// to the tap.
return;
}
}
}

final docPosition = _docLayout.getDocumentPositionNearestToOffset(docOffset);
editorGesturesLog.fine(" - tapped document position: $docPosition");
if (docPosition != null) {
// The user tapped a non-selectable component, so we can't select a paragraph.
// The editor will remain focused and selection will remain in the nearest
Expand Down
Loading

0 comments on commit 8ba09ff

Please sign in to comment.