diff --git a/example/lib/app.dart b/example/lib/app.dart index f3d9ca4..b6b2ad6 100644 --- a/example/lib/app.dart +++ b/example/lib/app.dart @@ -1,6 +1,9 @@ import 'package:flutter/material.dart'; +import 'package:languagetool_textfield/languagetool_textfield.dart'; +/// Example App main page class App extends StatefulWidget { + /// Example app constructor const App({super.key}); @override @@ -8,8 +11,37 @@ class App extends StatefulWidget { } class _AppState extends State { + /// Initialize LanguageTool + static final LanguageTool _languageTool = LanguageTool(); + + /// Initialize DebounceLangToolService + static final DebounceLangToolService _debouncedLangService = + DebounceLangToolService( + LangToolService(_languageTool), + const Duration(milliseconds: 500), + ); + + /// Initialize ColoredTextEditingController + final ColoredTextEditingController _controller = + ColoredTextEditingController(languageCheckService: _debouncedLangService); + @override Widget build(BuildContext context) { - return const Placeholder(); + return Material( + child: LanguageToolTextField( + style: const TextStyle(), + decoration: const InputDecoration(), + mistakeBuilder: () { + return Container(); + }, + coloredController: _controller, + ), + ); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); } } diff --git a/lib/core/controllers/colored_text_editing_controller.dart b/lib/core/controllers/colored_text_editing_controller.dart new file mode 100644 index 0000000..3eb93cd --- /dev/null +++ b/lib/core/controllers/colored_text_editing_controller.dart @@ -0,0 +1,128 @@ +import 'package:flutter/material.dart'; +import 'package:languagetool_textfield/core/enums/mistake_type.dart'; +import 'package:languagetool_textfield/domain/highlight_style.dart'; +import 'package:languagetool_textfield/domain/language_check_service.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 HighlightStyle highlightStyle; + + /// Language tool API index + final LanguageCheckService languageCheckService; + + /// List which contains Mistake objects spans are built from + List _mistakes = []; + + @override + set value(TextEditingValue newValue) { + _handleTextChange(newValue.text); + super.value = newValue; + } + + /// Controller constructor + ColoredTextEditingController({ + required this.languageCheckService, + this.highlightStyle = const HighlightStyle(), + }); + + /// Clear mistakes list when text mas modified and get a new list of mistakes + /// via API + Future _handleTextChange(String newText) async { + ///set value triggers each time, even when cursor changes its location + ///so this check avoid cleaning Mistake list when text wasn't really changed + if (newText.length == text.length) return; + _mistakes.clear(); + final mistakes = await languageCheckService.findMistakes(newText); + if (mistakes.isNotEmpty) { + _mistakes = mistakes; + notifyListeners(); + } + } + + /// Generates TextSpan from Mistake list + @override + TextSpan buildTextSpan({ + required BuildContext context, + TextStyle? style, + required bool withComposing, + }) { + final formattedTextSpans = _generateSpans( + style: style, + ); + + return TextSpan( + children: formattedTextSpans.toList(), + ); + } + + /// Generator function to create TextSpan instances + Iterable _generateSpans({ + TextStyle? style, + }) sync* { + int currentOffset = 0; // enter index + + for (final Mistake mistake in _mistakes) { + /// TextSpan before mistake + yield TextSpan( + text: text.substring( + currentOffset, + mistake.offset, + ), + style: style, + ); + + /// Get a highlight color + final Color mistakeColor = _getMistakeColor(mistake.type); + + /// Mistake highlighted TextSpan + yield TextSpan( + children: [ + TextSpan( + text: + text.substring(mistake.offset, mistake.offset + mistake.length), + mouseCursor: MaterialStateMouseCursor.clickable, + style: style?.copyWith( + backgroundColor: mistakeColor.withOpacity( + highlightStyle.backgroundOpacity, + ), + decoration: highlightStyle.decoration, + decorationColor: mistakeColor, + decorationThickness: highlightStyle.mistakeLineThickness, + ), + ), + ], + ); + + currentOffset = mistake.offset + mistake.length; + } + + /// TextSpan after mistake + yield TextSpan( + text: text.substring(currentOffset), + style: style, + ); + } + + /// Returns color for mistake TextSpan style + Color _getMistakeColor(MistakeType type) { + switch (type) { + case MistakeType.misspelling: + return highlightStyle.misspellingMistakeColor; + case MistakeType.typographical: + return highlightStyle.typographicalMistakeColor; + case MistakeType.grammar: + return highlightStyle.grammarMistakeColor; + case MistakeType.uncategorized: + return highlightStyle.uncategorizedMistakeColor; + case MistakeType.nonConformance: + return highlightStyle.nonConformanceMistakeColor; + case MistakeType.style: + return highlightStyle.styleMistakeColor; + case MistakeType.other: + return highlightStyle.otherMistakeColor; + } + } +} 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_style.dart b/lib/domain/highlight_style.dart new file mode 100644 index 0000000..6712d7d --- /dev/null +++ b/lib/domain/highlight_style.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; + +/// Class creates color scheme for highlighting mistakes +class HighlightStyle { + ///Initial values + static const double _initialBackgroundOpacity = 0.2; + static const double _initialLineHeight = 1.5; + + /// 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; + + /// background opacity for mistake TextSpan + final double backgroundOpacity; + + /// mistake TextSpan underline thickness + final double mistakeLineThickness; + + /// Mistaken text decoration style + final TextDecoration decoration; + + ///Color scheme constructor + const HighlightStyle({ + this.misspellingMistakeColor = Colors.red, + this.typographicalMistakeColor = Colors.green, + this.grammarMistakeColor = Colors.amber, + this.uncategorizedMistakeColor = Colors.blue, + this.nonConformanceMistakeColor = Colors.greenAccent, + this.styleMistakeColor = Colors.deepPurpleAccent, + this.otherMistakeColor = Colors.white60, + this.backgroundOpacity = _initialBackgroundOpacity, + this.mistakeLineThickness = _initialLineHeight, + this.decoration = TextDecoration.underline, + }); +} diff --git a/lib/domain/mistake.dart b/lib/domain/mistake.dart index ecbd3a8..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; diff --git a/lib/implementations/debounce_lang_tool_service.dart b/lib/implementations/debounce_lang_tool_service.dart index ed14755..7aabc2d 100644 --- a/lib/implementations/debounce_lang_tool_service.dart +++ b/lib/implementations/debounce_lang_tool_service.dart @@ -17,7 +17,11 @@ class DebounceLangToolService extends LanguageCheckService { ) : debouncing = Debouncing(duration: debouncingDuration); @override - Future> findMistakes(String text) => - debouncing.debounce(() => baseService.findMistakes(text)) - as Future>; + Future> findMistakes(String text) async { + final value = await debouncing.debounce(() { + return baseService.findMistakes(text); + }) as List?; + + return value ?? []; + } } 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 abe7c4c..1050872 100644 --- a/lib/languagetool_textfield.dart +++ b/lib/languagetool_textfield.dart @@ -1 +1,12 @@ library languagetool_textfield; + +export 'package:language_tool/language_tool.dart'; + +export 'core/controllers/colored_text_editing_controller.dart'; +export 'domain/highlight_style.dart'; +export 'domain/language_check_service.dart'; +export 'domain/mistake.dart'; +export 'implementations/debounce_lang_tool_service.dart'; +export 'implementations/lang_tool_service.dart'; +export 'implementations/throttling_lang_tool_service.dart'; +export "presentation/language_tool_text_field.dart"; diff --git a/lib/presentation/language_tool_text_field.dart b/lib/presentation/language_tool_text_field.dart index 0652baf..b51523c 100644 --- a/lib/presentation/language_tool_text_field.dart +++ b/lib/presentation/language_tool_text_field.dart @@ -1,11 +1,9 @@ import 'package:flutter/material.dart'; -import 'package:languagetool_textfield/domain/language_check_service.dart'; +import 'package:languagetool_textfield/core/controllers/colored_text_editing_controller.dart'; -/// A TextField widget that checks the grammar using the given [langService] +/// A TextField widget that checks the grammar using the given +/// [coloredController] class LanguageToolTextField extends StatefulWidget { - /// A service for checking errors. - final LanguageCheckService langService; - /// A style to use for the text being edited. final TextStyle style; @@ -15,13 +13,16 @@ class LanguageToolTextField extends StatefulWidget { /// A builder function used to build errors. final Widget Function()? mistakeBuilder; + /// Color scheme to highlight mistakes + final ColoredTextEditingController coloredController; + /// Creates a widget that checks grammar errors. const LanguageToolTextField({ Key? key, - required this.langService, required this.style, required this.decoration, this.mistakeBuilder, + required this.coloredController, }) : super(key: key); @override @@ -31,6 +32,15 @@ class LanguageToolTextField extends StatefulWidget { class _LanguageToolTextFieldState extends State { @override Widget build(BuildContext context) { - return const Placeholder(); + return Padding( + padding: const EdgeInsets.all(24.0), + child: Center( + child: TextField( + controller: widget.coloredController, + style: widget.style, + decoration: widget.decoration, + ), + ), + ); } }