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..9aec67e --- /dev/null +++ b/lib/core/controllers/colored_text_editing_controller.dart @@ -0,0 +1,107 @@ +import 'dart:developer'; + +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:languagetool_textfield/domain/mistake.dart'; + +/// A TextEditingController with overrides buildTextSpan for building +/// marked TextSpans with tap recognizer +class ColoredTextEditingController extends TextEditingController { + /// List which contains Mistake objects spans are built from + List _mistakes = []; + + 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(); + } + + /// builds TextSpan from Mistake list + @override + TextSpan buildTextSpan({ + required BuildContext context, + TextStyle? style, + 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( + text: text.substring( + currentOffset, + mistake.offset, + ), + style: style, + ), + ); + + /// Setting color of the mistake by its type + final Color mistakeColor = _getMistakeColor(mistake.type); + + /// The mistake TextSpan + spans.add( + 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), + decoration: TextDecoration.underline, + decorationColor: mistakeColor, + decorationThickness: _mistakeLineThickness, + ), + ), + ); + + /// Changing enter index position for the next iteration + currentOffset = mistake.offset + mistake.length; + } + + /// TextSpan after mistake + spans.add( + TextSpan( + text: text.substring(currentOffset), + style: style, + ), + ); + + /// Returns TextSpan + return TextSpan(children: spans); + } + + void _callOverlay(TapDownDetails details) { + log(details.globalPosition.toString()); + } + + /// Returns color for mistake TextSpan style + Color _getMistakeColor(String type) { + switch (type) { + case 'misspelling': + return Colors.red; + case 'style': + return Colors.blue; + case 'uncategorized': + return Colors.amber; + default: + return Colors.green; + } + } +} diff --git a/lib/domain/mistake.dart b/lib/domain/mistake.dart index ecbd3a8..9433887 100644 --- a/lib/domain/mistake.dart +++ b/lib/domain/mistake.dart @@ -25,4 +25,10 @@ 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/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/languagetool_textfield.dart b/lib/languagetool_textfield.dart index abe7c4c..d3178aa 100644 --- a/lib/languagetool_textfield.dart +++ b/lib/languagetool_textfield.dart @@ -1 +1,10 @@ library languagetool_textfield; + +export 'package:language_tool/language_tool.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..23e75df 100644 --- a/lib/presentation/language_tool_text_field.dart +++ b/lib/presentation/language_tool_text_field.dart @@ -1,4 +1,6 @@ + import 'package:flutter/material.dart'; +import 'package:languagetool_textfield/core/controllers/colored_text_editing_controller.dart'; import 'package:languagetool_textfield/domain/language_check_service.dart'; /// A TextField widget that checks the grammar using the given [langService] @@ -29,8 +31,37 @@ class LanguageToolTextField extends StatefulWidget { } class _LanguageToolTextFieldState extends State { + final ColoredTextEditingController _controller = + ColoredTextEditingController(); + final int _maxLines = 8; // max lines of the TextFiled + + /// 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); + } + } + + @override + void dispose() { + super.dispose(); + _controller.dispose(); // disposes controller + } + @override Widget build(BuildContext context) { - return const Placeholder(); + return Padding( + padding: const EdgeInsets.all(24.0), + child: Center( + child: TextField( + controller: _controller, + maxLines: _maxLines, + onChanged: _check, + style: widget.style, + decoration: widget.decoration, + ), + ), + ); } }