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

[SuperEditor][mobile] Implement spellchecking support (Resolves #2353) #2378

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
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
90 changes: 62 additions & 28 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 @@ -48,7 +49,7 @@ class DocumentMouseInteractor extends StatefulWidget {
required this.getDocumentLayout,
required this.selectionNotifier,
required this.selectionChanges,
this.contentTapHandler,
this.contentTapHandlers,
required this.autoScroller,
this.showDebugPaint = false,
this.child,
Expand All @@ -62,9 +63,9 @@ 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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For each Dart Doc related to these handlers, please specify the chain of responsibility behavior. The current Dart Docs here, and elsewhere, don't explain that only a single handler is ever run...

/// a link when the user taps on text with a link attribution.
final ContentTapDelegate? contentTapHandler;
final List<ContentTapDelegate>? contentTapHandlers;

/// Auto-scrolling delegate.
final AutoScrollController autoScroller;
Expand Down Expand Up @@ -118,7 +119,11 @@ 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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't remember what this callback is for. Are you sure it makes sense to run that callback for every handler?

}
}
}

@override
Expand All @@ -142,15 +147,29 @@ 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 @@ -255,12 +274,14 @@ class _DocumentMouseInteractorState extends State<DocumentMouseInteractor> with
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;
if (widget.contentTapHandlers != null) {
for (final handler in widget.contentTapHandlers!) {
final result = handler.onTap(docPosition);
if (result == TapHandlingInstruction.halt) {
// The custom tap handler doesn't want us to react at all
// to the tap.
return;
}
}
}

Expand Down Expand Up @@ -304,12 +325,14 @@ class _DocumentMouseInteractorState extends State<DocumentMouseInteractor> with
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 (docPosition != null && widget.contentTapHandlers != null) {
for (final handler in widget.contentTapHandlers!) {
final result = handler.onDoubleTap(docPosition);
if (result == TapHandlingInstruction.halt) {
// The custom tap handler doesn't want us to react at all
// to the tap.
return;
}
}
}

Expand Down Expand Up @@ -405,12 +428,14 @@ class _DocumentMouseInteractorState extends State<DocumentMouseInteractor> with
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 (docPosition != null && widget.contentTapHandlers != null) {
for (final handler in widget.contentTapHandlers!) {
final result = handler.onTripleTap(docPosition);
if (result == TapHandlingInstruction.halt) {
// The custom tap handler doesn't want us to react at all
// to the tap.
return;
}
}
}

Expand Down Expand Up @@ -741,8 +766,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 @@ -258,6 +258,29 @@ class SuperEditorAndroidControlsController {
}
}

/// {@template are_selection_handles_allowed}
/// Whether or not the selection handles are allowed to be displayed.
///
/// Typically, whenever the selection changes the drag handles are displayed. However,
/// there are some cases where we want to select some content, but don't show the
/// drag handles. For example, when the user taps a misspelled word, we might want to select
/// the misspelled word without showing any handles.
///
/// Defaults to `true`.
/// {@endtemplate}
ValueListenable<bool> get areSelectionHandlesAllowed => _areSelectionHandlesAllowed;
final _areSelectionHandlesAllowed = ValueNotifier<bool>(true);

/// Temporarily prevents any selection handles from being displayed.
///
/// Call this when you want to select some content, but don't want to show the drag handles.
/// [allowSelectionHandles] must be called to allow the drag handles to be displayed again.
void preventSelectionHandles() => _areSelectionHandlesAllowed.value = false;

/// Allows the selection handles to be displayed after they have been temporarily
/// prevented by [preventSelectionHandles].
void allowSelectionHandles() => _areSelectionHandlesAllowed.value = true;

/// (Optional) Builder to create the visual representation of the expanded drag handles.
///
/// If [expandedHandlesBuilder] is `null`, default Android handles are displayed.
Expand Down Expand Up @@ -406,7 +429,7 @@ class AndroidDocumentTouchInteractor extends StatefulWidget {
required this.selection,
required this.openSoftwareKeyboard,
required this.scrollController,
this.contentTapHandler,
this.contentTapHandlers,
this.dragAutoScrollBoundary = const AxisOffset.symmetric(54),
required this.dragHandleAutoScroller,
this.showDebugPaint = false,
Expand All @@ -423,9 +446,9 @@ 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 responds to taps on content, e.g., opening
/// a link when the user taps on text with a link attribution.
final ContentTapDelegate? contentTapHandler;
final List<ContentTapDelegate>? contentTapHandlers;

final ScrollController scrollController;

Expand Down Expand Up @@ -784,6 +807,23 @@ class _AndroidDocumentTouchInteractorState extends State<AndroidDocumentTouchInt
// Stop waiting for a long-press to start.
_tapDownLongPressTimer?.cancel();

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.contentTapHandlers != null && docPosition != null) {
for (final handler in widget.contentTapHandlers!) {
final result = handler.onTap(docPosition);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see that this was moved up above the long tap and scrolling handler. I'm not sure that's correct. Can you explain why they belong up here?

if (result == TapHandlingInstruction.halt) {
// The custom tap handler doesn't want us to react at all
// to the tap.
return;
}
}
}

// Cancel any on-going long-press.
if (_isLongPressInProgress) {
_longPressStrategy = null;
Expand All @@ -799,21 +839,6 @@ class _AndroidDocumentTouchInteractorState extends State<AndroidDocumentTouchInt
return;
}

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;
}
}

bool didTapOnExistingSelection = false;
if (docPosition != null) {
final selection = widget.selection.value;
Expand Down Expand Up @@ -863,12 +888,14 @@ class _AndroidDocumentTouchInteractorState extends State<AndroidDocumentTouchInt
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 && docPosition != null) {
for (final handler in widget.contentTapHandlers!) {
final result = handler.onDoubleTap(docPosition);
if (result == TapHandlingInstruction.halt) {
// The custom tap handler doesn't want us to react at all
// to the tap.
return;
}
}
}

Expand Down Expand Up @@ -937,12 +964,14 @@ class _AndroidDocumentTouchInteractorState extends State<AndroidDocumentTouchInt
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 && docPosition != null) {
for (final handler in widget.contentTapHandlers!) {
final result = handler.onTripleTap(docPosition);
if (result == TapHandlingInstruction.halt) {
// The custom tap handler doesn't want us to react at all
// to the tap.
return;
}
}
}

Expand Down Expand Up @@ -1730,6 +1759,7 @@ class SuperEditorAndroidControlsOverlayManagerState extends State<SuperEditorAnd
link: _controlsController!.collapsedHandleFocalPoint,
leaderAnchor: Alignment.bottomCenter,
followerAnchor: Alignment.topCenter,
showWhenUnlinked: false,
// Use the offset to account for the invisible expanded touch region around the handle.
offset: -Offset(0, AndroidSelectionHandle.defaultTouchRegionExpansion.top) *
MediaQuery.devicePixelRatioOf(context),
Expand Down Expand Up @@ -1780,6 +1810,7 @@ class SuperEditorAndroidControlsOverlayManagerState extends State<SuperEditorAnd
link: _controlsController!.upstreamHandleFocalPoint,
leaderAnchor: Alignment.bottomLeft,
followerAnchor: Alignment.topRight,
showWhenUnlinked: false,
// Use the offset to account for the invisible expanded touch region around the handle.
offset:
-AndroidSelectionHandle.defaultTouchRegionExpansion.topRight * MediaQuery.devicePixelRatioOf(context),
Expand Down Expand Up @@ -1812,6 +1843,7 @@ class SuperEditorAndroidControlsOverlayManagerState extends State<SuperEditorAnd
link: _controlsController!.downstreamHandleFocalPoint,
leaderAnchor: Alignment.bottomRight,
followerAnchor: Alignment.topLeft,
showWhenUnlinked: false,
// Use the offset to account for the invisible expanded touch region around the handle.
offset:
-AndroidSelectionHandle.defaultTouchRegionExpansion.topLeft * MediaQuery.devicePixelRatioOf(context),
Expand Down
Loading
Loading