diff --git a/lib/core/controllers/colored_text_editing_controller.dart b/lib/core/controllers/colored_text_editing_controller.dart index ce2f62a..4a7bff7 100644 --- a/lib/core/controllers/colored_text_editing_controller.dart +++ b/lib/core/controllers/colored_text_editing_controller.dart @@ -1,28 +1,33 @@ -import 'dart:developer'; - -import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; +import 'package:languagetool_textfield/core/enums/mistake_type.dart'; +import 'package:languagetool_textfield/domain/highlight_colors.dart'; import 'package:languagetool_textfield/domain/mistake.dart'; /// A TextEditingController with overrides buildTextSpan for building /// marked TextSpans with tap recognizer class ColoredTextEditingController extends TextEditingController { + /// Color scheme to highlight mistakes + final HighlightColors? highlightColorScheme; + /// List which contains Mistake objects spans are built from List _mistakes = []; - final double _backGroundOpacity = + final double _backgroundOpacity = 0.2; // background opacity for mistake TextSpan final double _mistakeLineThickness = 1.5; // mistake TextSpan underline thickness - /// A method sets new list of Mistake and triggers buildTextSpan - void setMistakes(List list) { - _mistakes = list; - notifyListeners(); + @override + set value(TextEditingValue newValue) { + _handleTextChange(newValue.text); + super.value = newValue; } - /// builds TextSpan from Mistake list + /// Controller constructor + ColoredTextEditingController({this.highlightColorScheme}); + + /// Generates TextSpan from Mistake list @override TextSpan buildTextSpan({ required BuildContext context, @@ -30,78 +35,120 @@ class ColoredTextEditingController extends TextEditingController { required bool withComposing, }) { int currentOffset = 0; // enter index - final List spans = []; // List of TextSpan - final int textLength = text.length; // Length of text to be built - - /// Iterates _mistakes and adds TextSpans from Mistake offset and length - for (final Mistake mistake in _mistakes) { - /// Breaks the loop if iterated Mistake offset is bigger than text length. - if (mistake.offset > textLength || - mistake.offset + mistake.length > textLength) { - break; - } - /// TextSpan before mistake - spans.add( - TextSpan( + final int textLength = text.length; + + /// Generator function to create TextSpan instances + Iterable generateSpans() sync* { + for (final Mistake mistake in _mistakes) { + /// Breaks the loop if iterated Mistake offset is bigger than text + /// length. + if (mistake.offset > textLength || + mistake.offset + mistake.length > textLength) { + break; + } + + /// TextSpan before mistake + yield TextSpan( text: text.substring( currentOffset, mistake.offset, ), style: style, - ), - ); + ); - /// Setting color of the mistake by its type - final Color mistakeColor = _getMistakeColor(mistake.type); + /// Get a highlight color + final Color mistakeColor = _getMistakeColor(mistake.type); - /// The mistake TextSpan - spans.add( - TextSpan( + /// Mistake highlighted TextSpan + yield TextSpan( text: text.substring(mistake.offset, mistake.offset + mistake.length), mouseCursor: MaterialStateMouseCursor.clickable, - recognizer: TapGestureRecognizer() - ..onTapDown = _callOverlay, // calls overlay with mistakes details style: style?.copyWith( - backgroundColor: mistakeColor.withOpacity(_backGroundOpacity), + backgroundColor: mistakeColor.withOpacity(_backgroundOpacity), decoration: TextDecoration.underline, decorationColor: mistakeColor, decorationThickness: _mistakeLineThickness, ), - ), - ); + ); - /// Changing enter index position for the next iteration - currentOffset = mistake.offset + mistake.length; - } + currentOffset = mistake.offset + mistake.length; + } - /// TextSpan after mistake - spans.add( - TextSpan( + /// TextSpan after mistake + yield TextSpan( text: text.substring(currentOffset), style: style, - ), - ); + ); + } - /// Returns TextSpan - return TextSpan(children: spans); + return TextSpan(children: generateSpans().toList()); } - void _callOverlay(TapDownDetails details) { - log(details.globalPosition.toString()); + /// Apply changes to Mistake list while new data being fetched + void _handleTextChange(String newText) { + final int deltaLength = newText.length - text.length; + + /// Update the _mistakes list in-place based on the text modifications + _mistakes = _mistakes + .map((mistake) { + int newOffset = mistake.offset; + int newLength = mistake.length; + + /// If the text modification starts within the mistake + if (selection.start >= mistake.offset && + selection.start <= mistake.offset + mistake.length) { + newLength += deltaLength; + } + + /// If the text modification starts before the mistake + else if (selection.start < mistake.offset) { + newOffset += deltaLength; + } + + /// Return the updated mistake (if the length is greater than 0) + return newLength > 0 + ? Mistake( + message: mistake.message, + type: mistake.type, + offset: newOffset, + length: newLength, + replacements: mistake.replacements, + ) + : null; + }) + .whereType() + .toList(); + + /// Notify listeners to rebuild the widget + notifyListeners(); + } + + /// A method sets new list of Mistake and triggers buildTextSpan + void highlightMistakes(List list) { + _mistakes = list; + notifyListeners(); } /// Returns color for mistake TextSpan style - Color _getMistakeColor(String type) { + Color _getMistakeColor(MistakeType type) { switch (type) { - case 'misspelling': - return Colors.red; - case 'style': - return Colors.blue; - case 'uncategorized': - return Colors.amber; - default: - return Colors.green; + case MistakeType.misspelling: + return highlightColorScheme?.misspellingMistakeColor ?? Colors.red; + case MistakeType.typographical: + return highlightColorScheme?.typographicalMistakeColor ?? Colors.green; + case MistakeType.grammar: + return highlightColorScheme?.grammarMistakeColor ?? Colors.amber; + case MistakeType.uncategorized: + return highlightColorScheme?.uncategorizedMistakeColor ?? Colors.blue; + case MistakeType.nonConformance: + return highlightColorScheme?.nonConformanceMistakeColor ?? + Colors.greenAccent; + case MistakeType.style: + return highlightColorScheme?.styleMistakeColor ?? + Colors.deepPurpleAccent; + case MistakeType.other: + return highlightColorScheme?.otherMistakeColor ?? Colors.white60; } } } diff --git a/lib/core/enums/mistake_type.dart b/lib/core/enums/mistake_type.dart new file mode 100644 index 0000000..513304c --- /dev/null +++ b/lib/core/enums/mistake_type.dart @@ -0,0 +1,23 @@ +///Enumerate several mistake types +enum MistakeType { + /// Misspelling mistake type + misspelling, + + /// Typographical mistake type + typographical, + + /// Grammar mistake type + grammar, + + /// Uncategorized mistake type + uncategorized, + + /// NonConformance mistake type + nonConformance, + + /// Style mistake type + style, + + /// Any other mistake type + other, +} diff --git a/lib/domain/highlight_colors.dart b/lib/domain/highlight_colors.dart new file mode 100644 index 0000000..29a3080 --- /dev/null +++ b/lib/domain/highlight_colors.dart @@ -0,0 +1,36 @@ +import 'dart:ui'; + +/// Class creates color scheme for highlighting mistakes +class HighlightColors { + /// Misspelling mistake highlight color + final Color? misspellingMistakeColor; + + /// Misspelling mistake highlight color + final Color? typographicalMistakeColor; + + /// Typographical mistake highlight color + final Color? grammarMistakeColor; + + /// Uncategorized mistake highlight color + final Color? uncategorizedMistakeColor; + + /// NonConformance mistake highlight color + final Color? nonConformanceMistakeColor; + + /// Style mistake highlight color + final Color? styleMistakeColor; + + /// Other mistake highlight color + final Color? otherMistakeColor; + + ///Color scheme constructor + HighlightColors( + this.misspellingMistakeColor, + this.typographicalMistakeColor, + this.grammarMistakeColor, + this.uncategorizedMistakeColor, + this.nonConformanceMistakeColor, + this.styleMistakeColor, + this.otherMistakeColor, + ); +} diff --git a/lib/domain/mistake.dart b/lib/domain/mistake.dart index 9433887..7c556b0 100644 --- a/lib/domain/mistake.dart +++ b/lib/domain/mistake.dart @@ -1,10 +1,12 @@ +import 'package:languagetool_textfield/core/enums/mistake_type.dart'; + /// A data model class that stores information about a single writing mistake. class Mistake { /// A brief description of the mistake. final String message; /// A type of this mistake. - final String type; + final MistakeType type; /// A position of the beginning of this mistake. final int offset; @@ -25,10 +27,4 @@ class Mistake { required this.length, this.replacements = const [], }); - - @override - String toString() { - return 'Mistake{message: $message, type: $type, offset: $offset, ' - 'length: $length, replacements: $replacements}'; - } } diff --git a/lib/implementations/lang_tool_service.dart b/lib/implementations/lang_tool_service.dart index 923b21c..60ea278 100644 --- a/lib/implementations/lang_tool_service.dart +++ b/lib/implementations/lang_tool_service.dart @@ -1,4 +1,5 @@ import 'package:language_tool/language_tool.dart'; +import 'package:languagetool_textfield/core/enums/mistake_type.dart'; import 'package:languagetool_textfield/domain/language_check_service.dart'; import 'package:languagetool_textfield/domain/mistake.dart'; @@ -16,7 +17,9 @@ class LangToolService extends LanguageCheckService { final mistakes = writingMistakes.map( (m) => Mistake( message: m.message, - type: m.issueType, + type: _stringToMistakeType( + m.issueType, + ), offset: m.offset, length: m.length, replacements: m.replacements, @@ -25,4 +28,23 @@ class LangToolService extends LanguageCheckService { return mistakes.toList(); } + + MistakeType _stringToMistakeType(String issueType) { + switch (issueType.toLowerCase()) { + case 'misspelling': + return MistakeType.misspelling; + case 'typographical': + return MistakeType.typographical; + case 'grammar': + return MistakeType.grammar; + case 'uncategorized': + return MistakeType.uncategorized; + case 'non-conformance': + return MistakeType.nonConformance; + case 'style': + return MistakeType.style; + default: + return MistakeType.other; + } + } } diff --git a/lib/languagetool_textfield.dart b/lib/languagetool_textfield.dart index d3178aa..b86e916 100644 --- a/lib/languagetool_textfield.dart +++ b/lib/languagetool_textfield.dart @@ -2,6 +2,7 @@ library languagetool_textfield; export 'package:language_tool/language_tool.dart'; +export 'domain/highlight_colors.dart'; export 'domain/language_check_service.dart'; export 'domain/mistake.dart'; export 'implementations/debounce_lang_tool_service.dart'; diff --git a/lib/presentation/language_tool_text_field.dart b/lib/presentation/language_tool_text_field.dart index 26c9df1..4f25664 100644 --- a/lib/presentation/language_tool_text_field.dart +++ b/lib/presentation/language_tool_text_field.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:languagetool_textfield/core/controllers/colored_text_editing_controller.dart'; +import 'package:languagetool_textfield/domain/highlight_colors.dart'; import 'package:languagetool_textfield/domain/language_check_service.dart'; /// A TextField widget that checks the grammar using the given [langService] @@ -16,6 +17,9 @@ class LanguageToolTextField extends StatefulWidget { /// A builder function used to build errors. final Widget Function()? mistakeBuilder; + /// Color scheme to highlight mistakes + final HighlightColors? highlightColorScheme; + /// Creates a widget that checks grammar errors. const LanguageToolTextField({ Key? key, @@ -23,6 +27,7 @@ class LanguageToolTextField extends StatefulWidget { required this.style, required this.decoration, this.mistakeBuilder, + this.highlightColorScheme, }) : super(key: key); @override @@ -30,22 +35,22 @@ class LanguageToolTextField extends StatefulWidget { } class _LanguageToolTextFieldState extends State { - final ColoredTextEditingController _controller = - ColoredTextEditingController(); - final int _maxLines = 8; // max lines of the TextFiled + ColoredTextEditingController? _controller; /// Sends API request to get a list of Mistake Future _check(String text) async { - final list = await widget.langService.findMistakes(text); - if (list.isNotEmpty) { - _controller.setMistakes(list); + final mistakes = await widget.langService.findMistakes(text); + if (mistakes.isNotEmpty) { + _controller?.highlightMistakes(mistakes); } } @override - void dispose() { - super.dispose(); - _controller.dispose(); // disposes controller + void initState() { + _controller = ColoredTextEditingController( + highlightColorScheme: widget.highlightColorScheme, + ); + super.initState(); } @override @@ -55,12 +60,21 @@ class _LanguageToolTextFieldState extends State { child: Center( child: TextField( controller: _controller, - maxLines: _maxLines, - onChanged: _check, + onChanged: (String text) { + if (_controller != null) { + _check(text); + } + }, style: widget.style, decoration: widget.decoration, ), ), ); } + + @override + void dispose() { + super.dispose(); + _controller?.dispose(); // disposes controller + } }