Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Created text editing controller which builds TextSpans from list of M… #19

Merged
merged 14 commits into from
Apr 28, 2023
Merged
34 changes: 33 additions & 1 deletion example/lib/app.dart
Original file line number Diff line number Diff line change
@@ -1,15 +1,47 @@
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
State<App> createState() => _AppState();
}

class _AppState extends State<App> {
/// 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();
}
}
129 changes: 129 additions & 0 deletions lib/core/controllers/colored_text_editing_controller.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
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<Mistake> _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<void> _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;
Comment on lines +34 to +36
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a really helpful comment here, good job!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks, I really appreciate it!

_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<TextSpan> _generateSpans({
// required int textLength,
nazarski marked this conversation as resolved.
Show resolved Hide resolved
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;
}
}
}
23 changes: 23 additions & 0 deletions lib/core/enums/mistake_type.dart
Original file line number Diff line number Diff line change
@@ -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,
}
52 changes: 52 additions & 0 deletions lib/domain/highlight_style.dart
Original file line number Diff line number Diff line change
@@ -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,
});
}
4 changes: 3 additions & 1 deletion lib/domain/mistake.dart
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
10 changes: 7 additions & 3 deletions lib/implementations/debounce_lang_tool_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,11 @@ class DebounceLangToolService extends LanguageCheckService {
) : debouncing = Debouncing(duration: debouncingDuration);

@override
Future<List<Mistake>> findMistakes(String text) =>
debouncing.debounce(() => baseService.findMistakes(text))
as Future<List<Mistake>>;
Future<List<Mistake>> findMistakes(String text) async {
final value = await debouncing.debounce(() {
return baseService.findMistakes(text);
}) as List<Mistake>?;

return value ?? [];
}
}
24 changes: 23 additions & 1 deletion lib/implementations/lang_tool_service.dart
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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,
Expand All @@ -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;
}
}
}
11 changes: 11 additions & 0 deletions lib/languagetool_textfield.dart
Original file line number Diff line number Diff line change
@@ -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";
24 changes: 17 additions & 7 deletions lib/presentation/language_tool_text_field.dart
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -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
Expand All @@ -31,6 +32,15 @@ class LanguageToolTextField extends StatefulWidget {
class _LanguageToolTextFieldState extends State<LanguageToolTextField> {
@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,
),
),
);
}
}