Skip to content

Commit 9f253a7

Browse files
angelosilvestrematthew-carroll
authored andcommitted
[SuperEditor][mobile] - Implement spellchecking support (Resolves #2353) (#2378)
1 parent f6a4fe8 commit 9f253a7

File tree

75 files changed

+2989
-106
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

75 files changed

+2989
-106
lines changed

super_editor/lib/src/default_editor/document_gestures_mouse.dart

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,6 @@ class _DocumentMouseInteractorState extends State<DocumentMouseInteractor> with
128128
widget.autoScroller
129129
..addListener(_updateDragSelection)
130130
..addListener(_updateMouseCursorAtLatestOffset);
131-
132131
if (widget.contentTapHandlers != null) {
133132
for (final handler in widget.contentTapHandlers!) {
134133
handler.addListener(_updateMouseCursorAtLatestOffset);

super_editor/lib/src/default_editor/document_gestures_touch_android.dart

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,29 @@ class SuperEditorAndroidControlsController {
259259
}
260260
}
261261

262+
/// {@template are_selection_handles_allowed}
263+
/// Whether or not the selection handles are allowed to be displayed.
264+
///
265+
/// Typically, whenever the selection changes the drag handles are displayed. However,
266+
/// there are some cases where we want to select some content, but don't show the
267+
/// drag handles. For example, when the user taps a misspelled word, we might want to select
268+
/// the misspelled word without showing any handles.
269+
///
270+
/// Defaults to `true`.
271+
/// {@endtemplate}
272+
ValueListenable<bool> get areSelectionHandlesAllowed => _areSelectionHandlesAllowed;
273+
final _areSelectionHandlesAllowed = ValueNotifier<bool>(true);
274+
275+
/// Temporarily prevents any selection handles from being displayed.
276+
///
277+
/// Call this when you want to select some content, but don't want to show the drag handles.
278+
/// [allowSelectionHandles] must be called to allow the drag handles to be displayed again.
279+
void preventSelectionHandles() => _areSelectionHandlesAllowed.value = false;
280+
281+
/// Allows the selection handles to be displayed after they have been temporarily
282+
/// prevented by [preventSelectionHandles].
283+
void allowSelectionHandles() => _areSelectionHandlesAllowed.value = true;
284+
262285
/// (Optional) Builder to create the visual representation of the expanded drag handles.
263286
///
264287
/// If [expandedHandlesBuilder] is `null`, default Android handles are displayed.
@@ -1752,6 +1775,7 @@ class SuperEditorAndroidControlsOverlayManagerState extends State<SuperEditorAnd
17521775
link: _controlsController!.collapsedHandleFocalPoint,
17531776
leaderAnchor: Alignment.bottomCenter,
17541777
followerAnchor: Alignment.topCenter,
1778+
showWhenUnlinked: false,
17551779
// Use the offset to account for the invisible expanded touch region around the handle.
17561780
offset: -Offset(0, AndroidSelectionHandle.defaultTouchRegionExpansion.top) *
17571781
MediaQuery.devicePixelRatioOf(context),
@@ -1822,6 +1846,7 @@ class SuperEditorAndroidControlsOverlayManagerState extends State<SuperEditorAnd
18221846
link: _controlsController!.upstreamHandleFocalPoint,
18231847
leaderAnchor: Alignment.bottomLeft,
18241848
followerAnchor: Alignment.topRight,
1849+
showWhenUnlinked: false,
18251850
// Use the offset to account for the invisible expanded touch region around the handle.
18261851
offset:
18271852
-AndroidSelectionHandle.defaultTouchRegionExpansion.topRight * MediaQuery.devicePixelRatioOf(context),
@@ -1852,6 +1877,7 @@ class SuperEditorAndroidControlsOverlayManagerState extends State<SuperEditorAnd
18521877
link: _controlsController!.downstreamHandleFocalPoint,
18531878
leaderAnchor: Alignment.bottomRight,
18541879
followerAnchor: Alignment.topLeft,
1880+
showWhenUnlinked: false,
18551881
// Use the offset to account for the invisible expanded touch region around the handle.
18561882
offset:
18571883
-AndroidSelectionHandle.defaultTouchRegionExpansion.topLeft * MediaQuery.devicePixelRatioOf(context),

super_editor/lib/src/default_editor/document_gestures_touch_ios.dart

Lines changed: 111 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,25 @@ class SuperEditorIosControlsController {
156156
/// Tells the caret to stop blinking by setting [shouldCaretBlink] to `false`.
157157
void doNotBlinkCaret() => _shouldCaretBlink.value = false;
158158

159+
/// {@macro are_selection_handles_allowed}
160+
ValueListenable<bool> get areSelectionHandlesAllowed => _areSelectionHandlesAllowed;
161+
final _areSelectionHandlesAllowed = ValueNotifier<bool>(true);
162+
163+
/// Temporarily prevents any selection handles from being displayed.
164+
///
165+
/// Call this when you want to select some content, but don't want to show the drag handles.
166+
/// [allowSelectionHandles] must be called to allow the drag handles to be displayed again.
167+
void allowSelectionHandles() => _areSelectionHandlesAllowed.value = true;
168+
169+
/// Allows the selection handles to be displayed after they have been temporarily
170+
/// prevented by [preventSelectionHandles].
171+
void preventSelectionHandles() => _areSelectionHandlesAllowed.value = false;
172+
173+
/// Reports the [HandleType] of the handle being dragged by the user.
174+
///
175+
/// If no drag handle is being dragged, this value is `null`.
176+
final ValueNotifier<HandleType?> handleBeingDragged = ValueNotifier<HandleType?>(null);
177+
159178
/// Controls the iOS floating cursor.
160179
late final FloatingCursorController floatingCursorController;
161180

@@ -580,14 +599,6 @@ class _IosDocumentTouchInteractorState extends State<IosDocumentTouchInteractor>
580599
..hideMagnifier()
581600
..blinkCaret();
582601

583-
final selection = widget.selection.value;
584-
if (selection != null &&
585-
!selection.isCollapsed &&
586-
(_isOverBaseHandle(details.localPosition) || _isOverExtentHandle(details.localPosition))) {
587-
_controlsController!.toggleToolbar();
588-
return;
589-
}
590-
591602
editorGesturesLog.info("Tap down on document");
592603
final docOffset = _interactorOffsetToDocumentOffset(details.localPosition);
593604
editorGesturesLog.fine(" - document offset: $docOffset");
@@ -609,6 +620,14 @@ class _IosDocumentTouchInteractorState extends State<IosDocumentTouchInteractor>
609620
}
610621
}
611622

623+
final selection = widget.selection.value;
624+
if (selection != null &&
625+
!selection.isCollapsed &&
626+
(_isOverBaseHandle(details.localPosition) || _isOverExtentHandle(details.localPosition))) {
627+
_controlsController!.toggleToolbar();
628+
return;
629+
}
630+
612631
final docPosition = _docLayout.getDocumentPositionNearestToOffset(docOffset);
613632
editorGesturesLog.fine(" - tapped document position: $docPosition");
614633
if (docPosition != null &&
@@ -713,13 +732,6 @@ class _IosDocumentTouchInteractorState extends State<IosDocumentTouchInteractor>
713732
}
714733

715734
void _onDoubleTapUp(TapUpDetails details) {
716-
final selection = widget.selection.value;
717-
if (selection != null &&
718-
!selection.isCollapsed &&
719-
(_isOverBaseHandle(details.localPosition) || _isOverExtentHandle(details.localPosition))) {
720-
return;
721-
}
722-
723735
editorGesturesLog.info("Double tap down on document");
724736
final docOffset = _interactorOffsetToDocumentOffset(details.localPosition);
725737
editorGesturesLog.fine(" - document offset: $docOffset");
@@ -741,6 +753,13 @@ class _IosDocumentTouchInteractorState extends State<IosDocumentTouchInteractor>
741753
}
742754
}
743755

756+
final selection = widget.selection.value;
757+
if (selection != null &&
758+
!selection.isCollapsed &&
759+
(_isOverBaseHandle(details.localPosition) || _isOverExtentHandle(details.localPosition))) {
760+
return;
761+
}
762+
744763
final docPosition = _docLayout.getDocumentPositionNearestToOffset(docOffset);
745764
editorGesturesLog.fine(" - tapped document position: $docPosition");
746765
if (docPosition != null) {
@@ -871,6 +890,24 @@ class _IosDocumentTouchInteractorState extends State<IosDocumentTouchInteractor>
871890
_globalTapDownOffset = null;
872891
_tapDownLongPressTimer?.cancel();
873892

893+
if (widget.contentTapHandlers != null) {
894+
final docOffset = _interactorOffsetToDocumentOffset(details.localPosition);
895+
for (final handler in widget.contentTapHandlers!) {
896+
final result = handler.onPanStart(
897+
DocumentTapDetails(
898+
documentLayout: _docLayout,
899+
layoutOffset: docOffset,
900+
globalOffset: details.globalPosition,
901+
),
902+
);
903+
if (result == TapHandlingInstruction.halt) {
904+
// The custom tap handler doesn't want us to react at all
905+
// to the tap.
906+
return;
907+
}
908+
}
909+
}
910+
874911
// TODO: to help the user drag handles instead of scrolling, try checking touch
875912
// placement during onTapDown, and then pick that up here. I think the little
876913
// bit of slop might be the problem.
@@ -957,6 +994,24 @@ class _IosDocumentTouchInteractorState extends State<IosDocumentTouchInteractor>
957994
}
958995

959996
void _onPanUpdate(DragUpdateDetails details) {
997+
if (widget.contentTapHandlers != null) {
998+
final docOffset = _interactorOffsetToDocumentOffset(details.localPosition);
999+
for (final handler in widget.contentTapHandlers!) {
1000+
final result = handler.onPanUpdate(
1001+
DocumentTapDetails(
1002+
documentLayout: _docLayout,
1003+
layoutOffset: docOffset,
1004+
globalOffset: details.globalPosition,
1005+
),
1006+
);
1007+
if (result == TapHandlingInstruction.halt) {
1008+
// The custom tap handler doesn't want us to react at all
1009+
// to the tap.
1010+
return;
1011+
}
1012+
}
1013+
}
1014+
9601015
_globalDragOffset = details.globalPosition;
9611016

9621017
_dragEndInInteractor = interactorBox.globalToLocal(details.globalPosition);
@@ -1003,6 +1058,7 @@ class _IosDocumentTouchInteractorState extends State<IosDocumentTouchInteractor>
10031058
const ClearComposingRegionRequest(),
10041059
]);
10051060
} else if (_dragHandleType == HandleType.upstream) {
1061+
_controlsController!.handleBeingDragged.value = HandleType.upstream;
10061062
widget.editor.execute([
10071063
ChangeSelectionRequest(
10081064
widget.selection.value!.copyWith(
@@ -1014,6 +1070,7 @@ class _IosDocumentTouchInteractorState extends State<IosDocumentTouchInteractor>
10141070
const ClearComposingRegionRequest(),
10151071
]);
10161072
} else if (_dragHandleType == HandleType.downstream) {
1073+
_controlsController!.handleBeingDragged.value = HandleType.downstream;
10171074
widget.editor.execute([
10181075
ChangeSelectionRequest(
10191076
widget.selection.value!.copyWith(
@@ -1028,9 +1085,28 @@ class _IosDocumentTouchInteractorState extends State<IosDocumentTouchInteractor>
10281085
}
10291086

10301087
void _onPanEnd(DragEndDetails details) {
1088+
if (widget.contentTapHandlers != null) {
1089+
final docOffset = _interactorOffsetToDocumentOffset(details.localPosition);
1090+
for (final handler in widget.contentTapHandlers!) {
1091+
final result = handler.onPanEnd(
1092+
DocumentTapDetails(
1093+
documentLayout: _docLayout,
1094+
layoutOffset: docOffset,
1095+
globalOffset: details.globalPosition,
1096+
),
1097+
);
1098+
if (result == TapHandlingInstruction.halt) {
1099+
// The custom tap handler doesn't want us to react at all
1100+
// to the tap.
1101+
return;
1102+
}
1103+
}
1104+
}
1105+
10311106
_controlsController!
10321107
..hideMagnifier()
1033-
..blinkCaret();
1108+
..blinkCaret()
1109+
..handleBeingDragged.value = null;
10341110

10351111
if (_dragMode != null) {
10361112
// The user was dragging a selection change in some way, either with handles
@@ -1040,9 +1116,21 @@ class _IosDocumentTouchInteractorState extends State<IosDocumentTouchInteractor>
10401116
}
10411117

10421118
void _onPanCancel() {
1119+
if (widget.contentTapHandlers != null) {
1120+
for (final handler in widget.contentTapHandlers!) {
1121+
final result = handler.onPanCancel();
1122+
if (result == TapHandlingInstruction.halt) {
1123+
// The custom tap handler doesn't want us to react at all
1124+
// to the tap.
1125+
return;
1126+
}
1127+
}
1128+
}
1129+
10431130
if (_dragMode != null) {
10441131
_onDragSelectionEnd();
10451132
}
1133+
_controlsController!.handleBeingDragged.value = null;
10461134
}
10471135

10481136
void _onDragSelectionEnd() {
@@ -1888,6 +1976,8 @@ class SuperEditorIosHandlesDocumentLayerBuilder implements SuperEditorLayerBuild
18881976
return const ContentLayerProxyWidget(child: SizedBox());
18891977
}
18901978

1979+
final controlsController = SuperEditorIosControlsScope.rootOf(context);
1980+
18911981
return IosHandlesDocumentLayer(
18921982
document: editContext.document,
18931983
documentLayout: editContext.documentLayout,
@@ -1898,13 +1988,13 @@ class SuperEditorIosHandlesDocumentLayerBuilder implements SuperEditorLayerBuild
18981988
const ClearComposingRegionRequest(),
18991989
]);
19001990
},
1901-
handleColor: handleColor ??
1902-
SuperEditorIosControlsScope.maybeRootOf(context)?.handleColor ??
1903-
Theme.of(context).primaryColor,
1991+
areSelectionHandlesAllowed: controlsController.areSelectionHandlesAllowed,
1992+
handleBeingDragged: controlsController.handleBeingDragged,
1993+
handleColor: handleColor ?? controlsController.handleColor ?? Theme.of(context).primaryColor,
19041994
caretWidth: caretWidth ?? 2,
19051995
handleBallDiameter: handleBallDiameter ?? defaultIosHandleBallDiameter,
1906-
shouldCaretBlink: SuperEditorIosControlsScope.rootOf(context).shouldCaretBlink,
1907-
floatingCursorController: SuperEditorIosControlsScope.rootOf(context).floatingCursorController,
1996+
shouldCaretBlink: controlsController.shouldCaretBlink,
1997+
floatingCursorController: controlsController.floatingCursorController,
19081998
);
19091999
}
19102000
}

super_editor/lib/src/default_editor/spelling_and_grammar/spelling_and_grammar_styler.dart

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ class SpellingAndGrammarStyler extends SingleColumnLayoutStylePhase {
1111
SpellingAndGrammarStyler({
1212
UnderlineStyle? spellingErrorUnderlineStyle,
1313
UnderlineStyle? grammarErrorUnderlineStyle,
14+
this.selectionHighlightColor,
1415
}) : _spellingErrorUnderlineStyle = spellingErrorUnderlineStyle,
1516
_grammarErrorUnderlineStyle = grammarErrorUnderlineStyle;
1617

@@ -34,6 +35,35 @@ class SpellingAndGrammarStyler extends SingleColumnLayoutStylePhase {
3435
markDirty();
3536
}
3637

38+
/// Whether or not we should override the default selection color with [selectionHighlightColor].
39+
///
40+
/// On mobile platforms, when the suggestions popover is opened, the selected text uses a different
41+
/// highlight color.
42+
bool _overrideSelectionColor = false;
43+
44+
/// The color to use for the selection highlight [overrideSelectionColor] is called.
45+
final Color? selectionHighlightColor;
46+
47+
/// Configure this styler to override the default selection color with [selectionHighlightColor].
48+
///
49+
/// The default editor selection styler phase configures a selection color for all selections.
50+
/// Call this method to use [selectionHighlightColor] instead. This is useful to highlight a
51+
/// selected misspelled word with a color that is different from the default selection color.
52+
///
53+
/// Call [useDefaultSelectionColor] to stop overriding the default selection color.
54+
void overrideSelectionColor() {
55+
_overrideSelectionColor = true;
56+
markDirty();
57+
}
58+
59+
/// Stop overriding the default selection color.
60+
///
61+
/// After calling this method, all selections will use the default selection color.
62+
void useDefaultSelectionColor() {
63+
_overrideSelectionColor = false;
64+
markDirty();
65+
}
66+
3767
final _errorsByNode = <String, Set<TextError>>{};
3868
final _dirtyNodes = <String>{};
3969

@@ -65,7 +95,7 @@ class SpellingAndGrammarStyler extends SingleColumnLayoutStylePhase {
6595
padding: viewModel.padding,
6696
componentViewModels: [
6797
for (final previousViewModel in viewModel.componentViewModels) //
68-
_applyErrors(previousViewModel),
98+
_applyErrors(previousViewModel.copy()),
6999
],
70100
);
71101

@@ -96,6 +126,10 @@ class SpellingAndGrammarStyler extends SingleColumnLayoutStylePhase {
96126
for (final spellingError in spellingErrors) spellingError.range,
97127
]);
98128

129+
if (_overrideSelectionColor && selectionHighlightColor != null) {
130+
viewModel.selectionColor = selectionHighlightColor!;
131+
}
132+
99133
final grammarErrors = _errorsByNode[viewModel.nodeId]!.where((error) => error.type == TextErrorType.grammar);
100134
if (_grammarErrorUnderlineStyle != null) {
101135
// The user explicitly requested this style be used for grammar errors.

0 commit comments

Comments
 (0)