From f11f566244ad2b559f4d8d46f1fd99ab3100dcda Mon Sep 17 00:00:00 2001 From: Angelo Silvestre Date: Mon, 28 Oct 2024 21:09:42 -0300 Subject: [PATCH] Cleanup --- .../document_gestures_touch_android.dart | 23 ++++++ .../document_gestures_touch_ios.dart | 7 ++ .../spelling_and_grammar_styler.dart | 15 ++-- .../lib/src/default_editor/super_editor.dart | 26 +++---- .../android/android_document_controls.dart | 2 + .../platforms/ios/ios_document_controls.dart | 4 +- .../spell_checker_popover_controller.dart | 56 +++++++++++---- .../spelling_and_grammar_plugin.dart | 71 +++++++++++-------- .../spelling_error_suggestion_overlay.dart | 62 ++++++---------- .../spelling_error_suggestions.dart | 1 - 10 files changed, 164 insertions(+), 103 deletions(-) diff --git a/super_editor/lib/src/default_editor/document_gestures_touch_android.dart b/super_editor/lib/src/default_editor/document_gestures_touch_android.dart index 2d9be66f36..4c04a180f3 100644 --- a/super_editor/lib/src/default_editor/document_gestures_touch_android.dart +++ b/super_editor/lib/src/default_editor/document_gestures_touch_android.dart @@ -259,6 +259,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 get areSelectionHandlesAllowed => _areSelectionHandlesAllowed; + final _areSelectionHandlesAllowed = ValueNotifier(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. diff --git a/super_editor/lib/src/default_editor/document_gestures_touch_ios.dart b/super_editor/lib/src/default_editor/document_gestures_touch_ios.dart index 20060ae2b5..87237eeb50 100644 --- a/super_editor/lib/src/default_editor/document_gestures_touch_ios.dart +++ b/super_editor/lib/src/default_editor/document_gestures_touch_ios.dart @@ -154,11 +154,18 @@ class SuperEditorIosControlsController { /// Tells the caret to stop blinking by setting [shouldCaretBlink] to `false`. void doNotBlinkCaret() => _shouldCaretBlink.value = false; + /// {@macro are_selection_handles_allowed} ValueListenable get areSelectionHandlesAllowed => _areSelectionHandlesAllowed; final _areSelectionHandlesAllowed = ValueNotifier(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 allowSelectionHandles() => _areSelectionHandlesAllowed.value = true; + /// Allows the selection handles to be displayed after they have been temporarily + /// prevented by [preventSelectionHandles]. void preventSelectionHandles() => _areSelectionHandlesAllowed.value = false; /// Controls the iOS floating cursor. diff --git a/super_editor/lib/src/default_editor/spelling_and_grammar/spelling_and_grammar_styler.dart b/super_editor/lib/src/default_editor/spelling_and_grammar/spelling_and_grammar_styler.dart index 0ddf8338be..2b6b4adab0 100644 --- a/super_editor/lib/src/default_editor/spelling_and_grammar/spelling_and_grammar_styler.dart +++ b/super_editor/lib/src/default_editor/spelling_and_grammar/spelling_and_grammar_styler.dart @@ -11,7 +11,6 @@ class SpellingAndGrammarStyler extends SingleColumnLayoutStylePhase { SpellingAndGrammarStyler({ UnderlineStyle? spellingErrorUnderlineStyle, UnderlineStyle? grammarErrorUnderlineStyle, - this.selectionStyles = defaultSelectionStyle, this.selectionHighlightColor = Colors.transparent, }) : _spellingErrorUnderlineStyle = spellingErrorUnderlineStyle, _grammarErrorUnderlineStyle = grammarErrorUnderlineStyle; @@ -36,8 +35,15 @@ class SpellingAndGrammarStyler extends SingleColumnLayoutStylePhase { markDirty(); } + /// Whether or not we should to override the default selection color with [selectionHighlightColor]. + /// + /// On mobile platforms, when the suggestions popover is opened, the selected text uses a different + /// highlight color. bool _overrideSelectionColor = false; + /// The color to use for the selection highlight [overrideSelectionColor] is called. + final Color selectionHighlightColor; + final _errorsByNode = >{}; final _dirtyNodes = {}; @@ -63,21 +69,18 @@ class SpellingAndGrammarStyler extends SingleColumnLayoutStylePhase { markDirty(); } + /// Temporarily use the [selectionHighlightColor] to override the default selection color. void overrideSelectionColor() { _overrideSelectionColor = true; markDirty(); } + /// Restore the default selection color. void useDefaultSelectionColor() { _overrideSelectionColor = false; markDirty(); } - //final SpellingErrorSuggestion? Function()? onGetCurrentSuggestion; - final Color selectionHighlightColor; - - final SelectionStyles selectionStyles; - @override SingleColumnLayoutViewModel style(Document document, SingleColumnLayoutViewModel viewModel) { final updatedViewModel = SingleColumnLayoutViewModel( diff --git a/super_editor/lib/src/default_editor/super_editor.dart b/super_editor/lib/src/default_editor/super_editor.dart index 7dcf8b1368..8116656e4f 100644 --- a/super_editor/lib/src/default_editor/super_editor.dart +++ b/super_editor/lib/src/default_editor/super_editor.dart @@ -115,7 +115,6 @@ class SuperEditor extends StatefulWidget { this.documentLayoutKey, Stylesheet? stylesheet, this.customStylePhases = const [], - this.prependedStylePhases = const [], this.appendedStylePhases = const [], List? componentBuilders, SelectionStyles? selectionStyle, @@ -238,7 +237,15 @@ class SuperEditor extends StatefulWidget { /// knows how to interpret and apply table styles for your visual table component. final List customStylePhases; - final List prependedStylePhases; + /// Custom style phases that are added to the very end of the style phases. + /// + /// Typically, apps should use [customStylePhases]. However, the selection + /// styles are always applied after [customStylePhases]. If you need to + /// change the selection color, add the style phase that changes the selection + /// color here. + /// + /// For example, a spellchecker might want to override the selection color + /// for misspelled words. final List appendedStylePhases; /// The `SuperEditor` input source, e.g., keyboard or Input Method Engine. @@ -292,15 +299,6 @@ class SuperEditor extends StatefulWidget { /// nor the default tap behavior will be executed. final List? contentTapDelegateFactories; - /// Shows/hides a popover with spelling suggestions. - /// - /// A [SpellCheckerPopoverDelegate] must be attached to this controller - /// before it can be used. - /// - /// The `SpellingAndGrammarPlugin` provides a default implementation for - /// a [SpellCheckerPopoverDelegate]. - //final SpellCheckerPopoverController? spellCheckerPopoverController; - /// Leader links that connect leader widgets near the user's selection /// to carets, handles, and other things that want to follow the selection. /// @@ -613,7 +611,6 @@ class SuperEditorState extends State { document: document, componentBuilders: widget.componentBuilders, pipeline: [ - ...widget.prependedStylePhases, _docStylesheetStyler, _docLayoutPerComponentBlockStyler, ...widget.customStylePhases, @@ -623,7 +620,8 @@ class SuperEditorState extends State { composingRegion: editContext.composer.composingRegion, showComposingUnderline: true, ), - // Selection changes are very volatile. Put that phase last + // Selection changes are very volatile. Put that phase last, + // just before the phases that the apps want to be at the end // to minimize view model recalculations. _docLayoutSelectionStyler, ...widget.appendedStylePhases, @@ -1188,6 +1186,8 @@ abstract class SuperEditorPlugin { /// Additional overlay [SuperEditorLayerBuilder]s that will be added to a given [SuperEditor]. List get documentOverlayBuilders => []; + /// Optional handler that responds to taps on content, e.g., opening + /// a link when the user taps on text with a link attribution. ContentTapDelegate? get contentTapDelegate => null; } diff --git a/super_editor/lib/src/infrastructure/platforms/android/android_document_controls.dart b/super_editor/lib/src/infrastructure/platforms/android/android_document_controls.dart index bb0640a960..0f36a9534c 100644 --- a/super_editor/lib/src/infrastructure/platforms/android/android_document_controls.dart +++ b/super_editor/lib/src/infrastructure/platforms/android/android_document_controls.dart @@ -174,6 +174,7 @@ class AndroidHandlesDocumentLayer extends DocumentLayoutLayerStatefulWidget { /// is retrieved from the root [SuperEditorAndroidControlsController]. final Color? caretColor; + /// {@macro are_selection_handles_allowed} final ValueListenable? areSelectionHandlesAllowed; final bool showDebugPaint; @@ -323,6 +324,7 @@ class AndroidControlsDocumentLayerState } if (widget.areSelectionHandlesAllowed?.value == false) { + // We don't want to show any selection handles. return null; } diff --git a/super_editor/lib/src/infrastructure/platforms/ios/ios_document_controls.dart b/super_editor/lib/src/infrastructure/platforms/ios/ios_document_controls.dart index 8878004a07..2666617c2e 100644 --- a/super_editor/lib/src/infrastructure/platforms/ios/ios_document_controls.dart +++ b/super_editor/lib/src/infrastructure/platforms/ios/ios_document_controls.dart @@ -516,6 +516,7 @@ class IosHandlesDocumentLayer extends DocumentLayoutLayerStatefulWidget { final void Function(DocumentSelection?, SelectionChangeType, String selectionReason) changeSelection; + /// {@macro are_selection_handles_allowed} final ValueListenable? areSelectionHandlesAllowed; /// Color the iOS-style text selection drag handles. @@ -725,7 +726,8 @@ class IosControlsDocumentLayerState extends DocumentLayoutLayerState null; } diff --git a/super_editor_spellcheck/lib/src/super_editor/spelling_and_grammar_plugin.dart b/super_editor_spellcheck/lib/src/super_editor/spelling_and_grammar_plugin.dart index 8435c8582d..038b902964 100644 --- a/super_editor_spellcheck/lib/src/super_editor/spelling_and_grammar_plugin.dart +++ b/super_editor_spellcheck/lib/src/super_editor/spelling_and_grammar_plugin.dart @@ -22,7 +22,6 @@ class SpellingAndGrammarPlugin extends SuperEditorPlugin { UnderlineStyle grammarErrorUnderlineStyle = defaultGrammarErrorUnderlineStyle, SpellingErrorSuggestionToolbarBuilder toolbarBuilder = defaultSpellingSuggestionToolbarBuilder, Color selectedWordHighlightColor = Colors.transparent, - SelectionStyles? selectionStyles, SuperEditorAndroidControlsController? androidControlsController, SuperEditorIosControlsController? iosControlsController, }) : _isSpellCheckEnabled = isSpellingCheckEnabled, @@ -37,21 +36,20 @@ class SpellingAndGrammarPlugin extends SuperEditorPlugin { ]; _styler = SpellingAndGrammarStyler( selectionHighlightColor: selectedWordHighlightColor, - selectionStyles: selectionStyles ?? defaultSelectionStyle, ); _contentTapDelegate = switch (defaultTargetPlatform) { - TargetPlatform.android => SuperEditorAndroidSpellCheckerTapHandler( + TargetPlatform.android => _SuperEditorAndroidSpellCheckerTapHandler( popoverController: _popoverController, controlsController: androidControlsController!, styler: _styler, ), - TargetPlatform.iOS => SuperEditorIosSpellCheckerTapHandler( + TargetPlatform.iOS => _SuperEditorIosSpellCheckerTapHandler( popoverController: _popoverController, controlsController: iosControlsController!, styler: _styler, ), - _ => SuperEditorDesktopSpellCheckerTapHandler(popoverController: _popoverController), + _ => _SuperEditorDesktopSpellCheckerTapHandler(popoverController: _popoverController), }; } @@ -423,7 +421,7 @@ class SpellingAndGrammarReaction implements EditReaction { _asyncRequestIds[textNode.id] = requestId; final suggestions = await _mobileSpellChecker.fetchSpellCheckSuggestions( - Locale('en', 'US'), + PlatformDispatcher.instance.locale, textNode.text.text, ); if (suggestions == null) { @@ -467,12 +465,16 @@ class SpellingAndGrammarReaction implements EditReaction { } } -class SuperEditorIosSpellCheckerTapHandler extends _SpellCheckerContentTapDelegate { - SuperEditorIosSpellCheckerTapHandler({ +/// A [ContentTapDelegate] that shows the suggestions popover when the user taps on +/// a misspelled word. +/// +/// When the suggestions popover is displayed, the selection expands to the whole word +/// and the selection handles are hidden. +class _SuperEditorIosSpellCheckerTapHandler extends _SpellCheckerContentTapDelegate { + _SuperEditorIosSpellCheckerTapHandler({ required this.popoverController, required this.controlsController, required this.styler, - super.editor, }); final SpellCheckerPopoverController popoverController; @@ -496,6 +498,7 @@ class SuperEditorIosSpellCheckerTapHandler extends _SpellCheckerContentTapDelega ..hideMagnifier() ..preventSelectionHandles(); + // Select the whole word. editor!.execute([ ChangeSelectionRequest( DocumentSelection( @@ -513,12 +516,11 @@ class SuperEditorIosSpellCheckerTapHandler extends _SpellCheckerContentTapDelega ), ]); - //controlsController. - - popoverController.show(DocumentSelection.collapsed(position: tapPosition)); - + // Change the selection color while the suggestions popover is visible. styler.overrideSelectionColor(); + popoverController.showSuggestions(spelling); + return TapHandlingInstruction.halt; } @@ -530,17 +532,21 @@ class SuperEditorIosSpellCheckerTapHandler extends _SpellCheckerContentTapDelega void _hideSpellCheckerPopover() { styler.useDefaultSelectionColor(); - popoverController.hide(); controlsController.allowSelectionHandles(); + popoverController.hide(); } } -class SuperEditorAndroidSpellCheckerTapHandler extends _SpellCheckerContentTapDelegate { - SuperEditorAndroidSpellCheckerTapHandler({ +/// A [ContentTapDelegate] that shows the suggestions popover when the user taps on +/// a misspelled word. +/// +/// When the suggestions popover is displayed, the selection and the composing region +/// expand to the whole word and the selection handles are hidden. +class _SuperEditorAndroidSpellCheckerTapHandler extends _SpellCheckerContentTapDelegate { + _SuperEditorAndroidSpellCheckerTapHandler({ required this.popoverController, required this.controlsController, required this.styler, - super.editor, }); final SpellCheckerPopoverController popoverController; @@ -553,8 +559,8 @@ class SuperEditorAndroidSpellCheckerTapHandler extends _SpellCheckerContentTapDe return TapHandlingInstruction.continueHandling; } - final suggestions = popoverController.findSuggestionsForWordAt(DocumentSelection.collapsed(position: tapPosition)); - if (suggestions == null) { + final spelling = popoverController.findSuggestionsForWordAt(DocumentSelection.collapsed(position: tapPosition)); + if (spelling == null || spelling.suggestions.isEmpty) { _hideSpellCheckerPopover(); return TapHandlingInstruction.continueHandling; } @@ -568,14 +574,17 @@ class SuperEditorAndroidSpellCheckerTapHandler extends _SpellCheckerContentTapDe final wordSelection = DocumentSelection( base: DocumentPosition( nodeId: tapPosition.nodeId, - nodePosition: TextNodePosition(offset: suggestions.range.start), + nodePosition: TextNodePosition(offset: spelling.range.start), ), extent: DocumentPosition( nodeId: tapPosition.nodeId, - nodePosition: TextNodePosition(offset: suggestions.range.end), + nodePosition: TextNodePosition(offset: spelling.range.end), ), ); + // Select the whole word and update the composing region to match + // the Android behavior of placing the whole word on the composing + // region when tapping at a word. editor!.execute([ ChangeSelectionRequest( wordSelection, @@ -585,10 +594,11 @@ class SuperEditorAndroidSpellCheckerTapHandler extends _SpellCheckerContentTapDe ChangeComposingRegionRequest(wordSelection), ]); - popoverController.show(DocumentSelection.collapsed(position: tapPosition)); - + // Change the selection color while the suggestion popover is visible. styler.overrideSelectionColor(); + popoverController.showSuggestions(spelling); + return TapHandlingInstruction.halt; } @@ -605,10 +615,11 @@ class SuperEditorAndroidSpellCheckerTapHandler extends _SpellCheckerContentTapDe } } -class SuperEditorDesktopSpellCheckerTapHandler extends _SpellCheckerContentTapDelegate { - SuperEditorDesktopSpellCheckerTapHandler({ +/// A [ContentTapDelegate] that shows the suggestions popover when the user taps on +/// a misspelled word. +class _SuperEditorDesktopSpellCheckerTapHandler extends _SpellCheckerContentTapDelegate { + _SuperEditorDesktopSpellCheckerTapHandler({ required this.popoverController, - super.editor, }); final SpellCheckerPopoverController popoverController; @@ -633,7 +644,7 @@ class SuperEditorDesktopSpellCheckerTapHandler extends _SpellCheckerContentTapDe ), ]); - popoverController.show(DocumentSelection.collapsed(position: tapPosition)); + popoverController.showSuggestions(spelling); return TapHandlingInstruction.halt; } @@ -649,10 +660,10 @@ class SuperEditorDesktopSpellCheckerTapHandler extends _SpellCheckerContentTapDe } } +/// A [ContentTapDelegate] that has access to the [editor] while the +/// plugin is attached to it. class _SpellCheckerContentTapDelegate extends ContentTapDelegate { - _SpellCheckerContentTapDelegate({ - this.editor, - }); + _SpellCheckerContentTapDelegate(); Editor? editor; } diff --git a/super_editor_spellcheck/lib/src/super_editor/spelling_error_suggestion_overlay.dart b/super_editor_spellcheck/lib/src/super_editor/spelling_error_suggestion_overlay.dart index 675c3e5508..4a31a930a5 100644 --- a/super_editor_spellcheck/lib/src/super_editor/spelling_error_suggestion_overlay.dart +++ b/super_editor_spellcheck/lib/src/super_editor/spelling_error_suggestion_overlay.dart @@ -88,12 +88,7 @@ class _SpellingErrorSuggestionOverlayState final _boundsKey = GlobalKey(); - //DocumentRange? _requestedRange; - SpellingErrorSuggestion? _spellingErrorSuggestionForRequestedRange; - - // DocumentRange? get _rangeToQuerySuggestions => widget.popoverController == null // - // ? widget.editor.context.composer.selection - // : _requestedRange; + SpellingErrorSuggestion? _currentSpellingSuggestions; @override void initState() { @@ -149,18 +144,16 @@ class _SpellingErrorSuggestionOverlayState } @override - void hideSuggestionsPopover() { + void showSuggestions(SpellingErrorSuggestion suggestions) { setState(() { - _spellingErrorSuggestionForRequestedRange = null; + _currentSpellingSuggestions = suggestions; }); } @override - void showSuggestionsForWordAt(DocumentRange targetRange) { + void hideSuggestionsPopover() { setState(() { - //_requestedRange = targetRange; - //_wantsToShowSuggestionsPopover = true; - _spellingErrorSuggestionForRequestedRange = _findSpellingSuggestionAtRange(widget.suggestions, targetRange); + _currentSpellingSuggestions = null; }); } @@ -181,13 +174,6 @@ class _SpellingErrorSuggestionOverlayState return spellingSuggestion; } - @override - void showSuggestions(SpellingErrorSuggestion suggestions) { - setState(() { - _spellingErrorSuggestionForRequestedRange = suggestions; - }); - } - void _onSelectionChange() { setState(() { // Re-compute layout data. The layout needs to be re-computed regardless @@ -232,6 +218,8 @@ class _SpellingErrorSuggestionOverlayState } void _onDocumentChange(DocumentChangeLog changeLog) { + // After the document changes, the currently visible suggestions + // might not be valid anymore. Hide the popover. hideSuggestionsPopover(); } @@ -267,27 +255,7 @@ class _SpellingErrorSuggestionOverlayState return null; } - // final range = _rangeToQuerySuggestions; - // if (range == null) { - // // No selection upon which to base spell check suggestions. - // return null; - // } - // if (range.start.nodeId != range.end.nodeId) { - // // Spelling error suggestions don't display when the user selects across nodes. - // return null; - // } - // if (range.end.nodePosition is! TextNodePosition) { - // // The user isn't selecting text. Fizzle. - // return null; - // } - - // final spellingSuggestion = _findSpellingSuggestionAtRange(widget.suggestions, range); - // if (spellingSuggestion == null) { - // // No selected mis-spelled word. Fizzle. - // return null; - // } - - final spellingSuggestion = _spellingErrorSuggestionForRequestedRange; + final spellingSuggestion = _currentSpellingSuggestions; if (spellingSuggestion == null) { // No selected mis-spelled word. Fizzle. return null; @@ -488,6 +456,8 @@ typedef SpellingErrorSuggestionToolbarBuilder = Widget Function( required Rect selectedWordBounds, }); +/// Creates a spelling suggestion toolbar depending on the +/// current platform. Widget defaultSpellingSuggestionToolbarBuilder( BuildContext context, { required FocusNode editorFocusNode, @@ -700,6 +670,12 @@ class _DesktopSpellingSuggestionToolbarState extends State