blob: 6ebefb6d3a11afc77aeaa4ae377ec22092857b3f [file] [log] [blame]
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/foundation.dart';
import 'package:flutter/painting.dart';
import 'package:flutter/services.dart'
show SpellCheckResults, SpellCheckService, SuggestionSpan, TextEditingValue;
/// Controls how spell check is performed for text input.
///
/// This configuration determines the [SpellCheckService] used to fetch the
/// [List<SuggestionSpan>] spell check results and the [TextStyle] used to
/// mark misspelled words within text input.
@immutable
class SpellCheckConfiguration {
/// Creates a configuration that specifies the service and suggestions handler
/// for spell check.
const SpellCheckConfiguration({
this.spellCheckService,
this.misspelledTextStyle,
}) : _spellCheckEnabled = true;
/// Creates a configuration that disables spell check.
const SpellCheckConfiguration.disabled()
: _spellCheckEnabled = false,
spellCheckService = null,
misspelledTextStyle = null;
/// The service used to fetch spell check results for text input.
final SpellCheckService? spellCheckService;
/// Style used to indicate misspelled words.
///
/// This is nullable to allow style-specific wrappers of [EditableText]
/// to infer this, but this must be specified if this configuration is
/// provided directly to [EditableText] or its construction will fail with an
/// assertion error.
final TextStyle? misspelledTextStyle;
final bool _spellCheckEnabled;
/// Whether or not the configuration should enable or disable spell check.
bool get spellCheckEnabled => _spellCheckEnabled;
/// Returns a copy of the current [SpellCheckConfiguration] instance with
/// specified overrides.
SpellCheckConfiguration copyWith({
SpellCheckService? spellCheckService,
TextStyle? misspelledTextStyle}) {
if (!_spellCheckEnabled) {
// A new configuration should be constructed to enable spell check.
return const SpellCheckConfiguration.disabled();
}
return SpellCheckConfiguration(
spellCheckService: spellCheckService ?? this.spellCheckService,
misspelledTextStyle: misspelledTextStyle ?? this.misspelledTextStyle,
);
}
@override
String toString() {
return '''
spell check enabled : $_spellCheckEnabled
spell check service : $spellCheckService
misspelled text style : $misspelledTextStyle
'''
.trim();
}
@override
bool operator ==(Object other) {
if (identical(this, other)) {
return true;
}
return other is SpellCheckConfiguration
&& other.spellCheckService == spellCheckService
&& other.misspelledTextStyle == misspelledTextStyle
&& other._spellCheckEnabled == _spellCheckEnabled;
}
@override
int get hashCode => Object.hash(spellCheckService, misspelledTextStyle, _spellCheckEnabled);
}
// Methods for displaying spell check results:
/// Adjusts spell check results to correspond to [newText] if the only results
/// that the handler has access to are the [results] corresponding to
/// [resultsText].
///
/// Used in the case where the request for the spell check results of the
/// [newText] is lagging in order to avoid display of incorrect results.
List<SuggestionSpan> _correctSpellCheckResults(
String newText, String resultsText, List<SuggestionSpan> results) {
final List<SuggestionSpan> correctedSpellCheckResults = <SuggestionSpan>[];
int spanPointer = 0;
int offset = 0;
int foundIndex;
int spanLength;
SuggestionSpan currentSpan;
SuggestionSpan adjustedSpan;
String currentSpanText;
String newSpanText = '';
bool currentSpanValid = false;
RegExp regex;
// Assumes that the order of spans has not been jumbled for optimization
// purposes, and will only search since the previously found span.
int searchStart = 0;
while (spanPointer < results.length) {
// Try finding SuggestionSpan from old results (currentSpan) in new text.
currentSpan = results[spanPointer];
currentSpanText =
resultsText.substring(currentSpan.range.start, currentSpan.range.end);
try {
// currentSpan was found and can be applied to new text.
newSpanText = newText.substring(
currentSpan.range.start + offset, currentSpan.range.end + offset);
currentSpanValid = true;
} catch (e) {
// currentSpan is invalid and needs to be searched for in newText.
currentSpanValid = false;
}
if (currentSpanValid && newSpanText == currentSpanText) {
// currentSpan was found at the same index in new text and old text
// (resultsText), so apply it to new text by adding it to the list of
// corrected results.
searchStart = currentSpan.range.end + offset;
adjustedSpan = SuggestionSpan(
TextRange(
start: currentSpan.range.start + offset, end: searchStart),
currentSpan.suggestions
);
correctedSpellCheckResults.add(adjustedSpan);
} else {
// Search for currentSpan in new text and if found, apply it to new text
// by adding it to the list of corrected results.
regex = RegExp('\\b$currentSpanText\\b');
foundIndex = newText.substring(searchStart).indexOf(regex);
if (foundIndex >= 0) {
foundIndex += searchStart;
spanLength = currentSpan.range.end - currentSpan.range.start;
searchStart = foundIndex + spanLength;
adjustedSpan = SuggestionSpan(
TextRange(start: foundIndex, end: searchStart),
currentSpan.suggestions
);
offset = foundIndex - currentSpan.range.start;
correctedSpellCheckResults.add(adjustedSpan);
}
}
spanPointer++;
}
return correctedSpellCheckResults;
}
/// Builds the [TextSpan] tree given the current state of the text input and
/// spell check results.
///
/// The [value] is the current [TextEditingValue] requested to be rendered
/// by a text input widget. The [composingWithinCurrentTextRange] value
/// represents whether or not there is a valid composing region in the
/// [value]. The [style] is the [TextStyle] to render the [value]'s text with,
/// and the [misspelledTextStyle] is the [TextStyle] to render misspelled
/// words within the [value]'s text with. The [spellCheckResults] are the
/// results of spell checking the [value]'s text.
TextSpan buildTextSpanWithSpellCheckSuggestions(
TextEditingValue value,
bool composingWithinCurrentTextRange,
TextStyle? style,
TextStyle misspelledTextStyle,
SpellCheckResults spellCheckResults) {
List<SuggestionSpan> spellCheckResultsSpans =
spellCheckResults.suggestionSpans;
final String spellCheckResultsText = spellCheckResults.spellCheckedText;
if (spellCheckResultsText != value.text) {
spellCheckResultsSpans = _correctSpellCheckResults(
value.text, spellCheckResultsText, spellCheckResultsSpans);
}
return TextSpan(
style: style,
children: _buildSubtreesWithMisspelledWordsIndicated(
spellCheckResultsSpans,
value,
style,
misspelledTextStyle,
composingWithinCurrentTextRange
)
);
}
/// Builds [TextSpan] subtree for text with misspelled words.
List<TextSpan> _buildSubtreesWithMisspelledWordsIndicated(
List<SuggestionSpan>? spellCheckSuggestions,
TextEditingValue value,
TextStyle? style,
TextStyle misspelledStyle,
bool composingWithinCurrentTextRange) {
final List<TextSpan> tsTreeChildren = <TextSpan>[];
int textPointer = 0;
int currSpanPointer = 0;
int endIndex;
SuggestionSpan currSpan;
final String text = value.text;
final TextRange composingRegion = value.composing;
final TextStyle composingTextStyle =
style?.merge(const TextStyle(decoration: TextDecoration.underline)) ??
const TextStyle(decoration: TextDecoration.underline);
final TextStyle misspelledJointStyle =
style?.merge(misspelledStyle) ?? misspelledStyle;
bool textPointerWithinComposingRegion = false;
bool currSpanIsComposingRegion = false;
// Add text interwoven with any misspelled words to the tree.
if (spellCheckSuggestions != null) {
while (textPointer < text.length &&
currSpanPointer < spellCheckSuggestions.length) {
currSpan = spellCheckSuggestions[currSpanPointer];
if (currSpan.range.start > textPointer) {
endIndex = currSpan.range.start < text.length
? currSpan.range.start
: text.length;
textPointerWithinComposingRegion =
composingRegion.start >= textPointer &&
composingRegion.end <= endIndex &&
!composingWithinCurrentTextRange;
if (textPointerWithinComposingRegion) {
_addComposingRegionTextSpans(tsTreeChildren, text, textPointer,
composingRegion, style, composingTextStyle);
tsTreeChildren.add(
TextSpan(
style: style,
text: text.substring(composingRegion.end, endIndex)
)
);
} else {
tsTreeChildren.add(
TextSpan(
style: style,
text: text.substring(textPointer, endIndex)
)
);
}
textPointer = endIndex;
} else {
endIndex =
currSpan.range.end < text.length ? currSpan.range.end : text.length;
currSpanIsComposingRegion = textPointer >= composingRegion.start &&
endIndex <= composingRegion.end &&
!composingWithinCurrentTextRange;
tsTreeChildren.add(
TextSpan(
style: currSpanIsComposingRegion
? composingTextStyle
: misspelledJointStyle,
text: text.substring(currSpan.range.start, endIndex)
)
);
textPointer = endIndex;
currSpanPointer++;
}
}
}
// Add any remaining text to the tree if applicable.
if (textPointer < text.length) {
if (textPointer < composingRegion.start &&
!composingWithinCurrentTextRange) {
_addComposingRegionTextSpans(tsTreeChildren, text, textPointer,
composingRegion, style, composingTextStyle);
if (composingRegion.end != text.length) {
tsTreeChildren.add(
TextSpan(
style: style,
text: text.substring(composingRegion.end, text.length)
)
);
}
} else {
tsTreeChildren.add(
TextSpan(
style: style, text: text.substring(textPointer, text.length)
)
);
}
}
return tsTreeChildren;
}
/// Helper method to create [TextSpan] tree children for specified range of
/// text up to and including the composing region.
void _addComposingRegionTextSpans(
List<TextSpan> treeChildren,
String text,
int start,
TextRange composingRegion,
TextStyle? style,
TextStyle composingTextStyle) {
treeChildren.add(
TextSpan(
style: style,
text: text.substring(start, composingRegion.start)
)
);
treeChildren.add(
TextSpan(
style: composingTextStyle,
text: text.substring(composingRegion.start, composingRegion.end)
)
);
}