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
26 changes: 25 additions & 1 deletion example/lib/app.dart
Original file line number Diff line number Diff line change
@@ -1,15 +1,39 @@
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
final DebounceLangToolService _debouncedLangService;

/// Set DebounceLangToolService
_AppState()
: _debouncedLangService = DebounceLangToolService(
LangToolService(_languageTool),
const Duration(milliseconds: 500),
);
@override
Widget build(BuildContext context) {
return const Placeholder();
return Material(
child: LanguageToolTextField(
langService: _debouncedLangService,
style: const TextStyle(),
decoration: const InputDecoration(),
mistakeBuilder: () {
return Container();
},
),
);
}
}
107 changes: 107 additions & 0 deletions lib/core/controllers/colored_text_editing_controller.dart
Original file line number Diff line number Diff line change
@@ -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<Mistake> _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<Mistake> list) {
_mistakes = list;
nazarski marked this conversation as resolved.
Show resolved Hide resolved
notifyListeners();
}

/// builds TextSpan from Mistake list
@override
TextSpan buildTextSpan({
required BuildContext context,
TextStyle? style,
required bool withComposing,
}) {
int currentOffset = 0; // enter index
final List<TextSpan> spans = []; // List of TextSpan
nazarski marked this conversation as resolved.
Show resolved Hide resolved
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(
nazarski marked this conversation as resolved.
Show resolved Hide resolved
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
nazarski marked this conversation as resolved.
Show resolved Hide resolved
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':
nazarski marked this conversation as resolved.
Show resolved Hide resolved
return Colors.red;
nazarski marked this conversation as resolved.
Show resolved Hide resolved
case 'style':
return Colors.blue;
case 'uncategorized':
return Colors.amber;
default:
return Colors.green;
}
}
}
6 changes: 6 additions & 0 deletions lib/domain/mistake.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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}';
}
}
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 ?? [];
}
}
9 changes: 9 additions & 0 deletions lib/languagetool_textfield.dart
Original file line number Diff line number Diff line change
@@ -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";
32 changes: 31 additions & 1 deletion lib/presentation/language_tool_text_field.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
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]
Expand Down Expand Up @@ -29,8 +30,37 @@ class LanguageToolTextField extends StatefulWidget {
}

class _LanguageToolTextFieldState extends State<LanguageToolTextField> {
final ColoredTextEditingController _controller =
ColoredTextEditingController();
final int _maxLines = 8; // max lines of the TextFiled
nazarski marked this conversation as resolved.
Show resolved Hide resolved

/// Sends API request to get a list of Mistake
Future<void> _check(String text) async {
final list = await widget.langService.findMistakes(text);
if (list.isNotEmpty) {
_controller.setMistakes(list);
}
}
nazarski marked this conversation as resolved.
Show resolved Hide resolved

@override
void dispose() {
super.dispose();
_controller.dispose(); // disposes controller
}
nazarski marked this conversation as resolved.
Show resolved Hide resolved

@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,
),
),
);
}
}