-
Notifications
You must be signed in to change notification settings - Fork 246
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
base: main
Are you sure you want to change the base?
Changes from all commits
dd633ff
ca6e359
448f087
3f48fbf
9a56415
ec6830d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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'; | ||
|
@@ -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, | ||
|
@@ -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 | ||
/// 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; | ||
|
@@ -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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
@@ -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(); | ||
} | ||
|
@@ -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; | ||
} | ||
} | ||
} | ||
|
||
|
@@ -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; | ||
} | ||
} | ||
} | ||
|
||
|
@@ -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; | ||
} | ||
} | ||
} | ||
|
||
|
@@ -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 | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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. | ||
|
@@ -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, | ||
|
@@ -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; | ||
|
||
|
@@ -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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
|
@@ -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; | ||
|
@@ -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; | ||
} | ||
} | ||
} | ||
|
||
|
@@ -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; | ||
} | ||
} | ||
} | ||
|
||
|
@@ -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), | ||
|
@@ -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), | ||
|
@@ -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), | ||
|
There was a problem hiding this comment.
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...