Skip to content

Commit

Permalink
Merge pull request #19 from solid-software/buld_text_spans
Browse files Browse the repository at this point in the history
Created text editing controller which builds TextSpans from list of M…
  • Loading branch information
solid-yuriiprykhodko committed Apr 28, 2023
2 parents 5ca884f + 096d049 commit 9833fb9
Show file tree
Hide file tree
Showing 9 changed files with 297 additions and 13 deletions.
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();
}
}
128 changes: 128 additions & 0 deletions lib/core/controllers/colored_text_editing_controller.dart
Original file line number Diff line number Diff line change
@@ -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<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;
_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({
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,
),
),
);
}
}

0 comments on commit 9833fb9

Please sign in to comment.