Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@
- potentially BREAKING: hide baseService and debouncing/throttling from Debouncing and Throttling LanguageService wrappers
- potentially BREAKING: rename `DebounceLanguageToolService` to `DebounceLanguageCheckService`
- potentially BREAKING: rename `ThrottleLanguageToolService` to `ThrottleLanguageCheckService`
- Support adding words to dictionary through `addWordToDictionary` callback in `LanguageToolMistakePopup`
- Allow overriding `languageCheckService`
- Add `isEnabled` to toggle spell check
- Use default IconButton padding for mistake popup
- Add missing properties from flutter's `TextField`
- autofillHints
- autofocus
Expand Down
163 changes: 139 additions & 24 deletions example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,55 +20,170 @@ class App extends StatefulWidget {
}

class _AppState extends State<App> {
/// Initialize LanguageToolController
final LanguageToolController _controller = LanguageToolController();
Set<String> _dictionary = {};
final _addWordController = TextEditingController();

static const List<MainAxisAlignment> alignments = [
MainAxisAlignment.center,
MainAxisAlignment.start,
MainAxisAlignment.end,
];
int currentAlignmentIndex = 0;
LanguageToolController? _spellCheckController;

LanguageToolController _nonNullController() {
return _spellCheckController ??= LanguageToolController(
languageCheckService: InMemoryDictionaryLanguageCheckService(
getDictionary: () => _dictionary,
),
);
}

@override
Widget build(BuildContext context) {
final spellCheckController = _nonNullController();

return Material(
child: Scaffold(
body: Column(
Comment thread
andrew-bekhiet-solid marked this conversation as resolved.
Outdated
mainAxisAlignment: alignments[currentAlignmentIndex],
mainAxisAlignment: MainAxisAlignment.start,
children: [
LanguageToolTextField(
controller: _controller,
controller: spellCheckController,
language: 'en-US',
mistakePopup: MistakePopup(
popupRenderer: PopupOverlayRenderer(),
mistakeBuilder: _mistakeBuilder,
),
),
ValueListenableBuilder(
valueListenable: _controller,
valueListenable: spellCheckController,
builder: (_, __, ___) => CheckboxListTile(
title: const Text("Enable spell checking"),
value: _controller.isEnabled,
onChanged: (value) => _controller.isEnabled = value ?? false,
value: spellCheckController.isEnabled,
onChanged: (value) =>
spellCheckController.isEnabled = value ?? false,
),
),
DropdownMenu(
hintText: "Select alignment...",
onSelected: (value) => setState(() {
currentAlignmentIndex = value ?? 0;
}),
dropdownMenuEntries: const [
DropdownMenuEntry(value: 0, label: "Center alignment"),
DropdownMenuEntry(value: 1, label: "Top alignment"),
DropdownMenuEntry(value: 2, label: "Bottom alignment"),
],
const SizedBox(height: 20),
Card(
margin: const EdgeInsets.all(16),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Dictionary',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: TextField(
controller: _addWordController,
decoration: const InputDecoration(
labelText: 'Add word to dictionary',
border: OutlineInputBorder(),
),
onSubmitted: (_) => _addWord(),
),
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: _addWord,
child: const Text('Add'),
),
],
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Dictionary Words (${_dictionary.length})',
style: const TextStyle(fontWeight: FontWeight.w500),
),
if (_dictionary.isNotEmpty)
TextButton(
onPressed: _clearAllWords,
child: const Text('Clear All'),
),
],
),
const SizedBox(height: 8),
if (_dictionary.isEmpty)
const Center(
child: Text(
'No words in dictionary',
style: TextStyle(color: Colors.grey),
),
)
else
for (final word in _dictionary)
ListTile(
title: Text(word),
trailing: IconButton(
icon: const Icon(Icons.delete),
onPressed: () => _removeWord(word),
),
),
],
),
),
),
],
),
),
);
}

void _addWord() {
final word = _addWordController.text.trim();

if (word.isNotEmpty && !_dictionary.contains(word)) {
setState(() {
_dictionary = {..._dictionary, word};
_addWordController.clear();
_spellCheckController?.recheckText();
});
}
}

void _removeWord(String word) {
setState(() {
_dictionary = _dictionary.difference({word});
_spellCheckController?.recheckText();
});
}

void _clearAllWords() {
setState(() {
_dictionary = {};
_spellCheckController?.recheckText();
});
}

Widget _mistakeBuilder({
required LanguageToolController controller,
required Mistake mistake,
required Offset mistakePosition,
required PopupOverlayRenderer popupRenderer,
}) {
return LanguageToolMistakePopup(
popupRenderer: popupRenderer,
mistake: mistake,
mistakePosition: mistakePosition,
controller: controller,
addWordToDictionary: (word) async {
setState(() => _dictionary = {..._dictionary, word});
},
);
}

@override
void dispose() {
_controller.dispose();
_spellCheckController?.dispose();
_addWordController.dispose();
super.dispose();
}
}
2 changes: 2 additions & 0 deletions lib/languagetool_textfield.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,7 @@ export 'src/language_check_services/language_tool_service.dart';
export 'src/presentation/language_tool_text_field.dart';
export 'src/utils/mistake_popup.dart';
export 'src/utils/popup_overlay_renderer.dart';
export 'src/utils/result.dart';
export 'src/wrappers/debounce_language_check_service.dart';
export 'src/wrappers/in_memory_dictionary_language_check_service.dart';
export 'src/wrappers/throttling_language_check_service.dart';
12 changes: 11 additions & 1 deletion lib/src/core/controllers/language_tool_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ class LanguageToolController extends TextEditingController {
_isEnabled = value;

if (_isEnabled) {
_handleTextChange(text, spellCheckSameText: true);
recheckText();
} else {
_mistakes = [];
for (final recognizer in _recognizers) {
Expand Down Expand Up @@ -182,6 +182,16 @@ class LanguageToolController extends TextEditingController {
});
}

/// Rechecks the current text for spelling and grammar errors.
///
/// This method forces a recheck of the existing text
/// This is useful when you want to re-evaluate the text without any actual
/// text changes, such as after changing language settings or updating
/// spell check configurations.
void recheckText() {
_handleTextChange(text, spellCheckSameText: true);
}

/// Clear mistakes list when text mas modified and get a new list of mistakes
/// via API
Future<void> _handleTextChange(
Expand Down
109 changes: 64 additions & 45 deletions lib/src/utils/mistake_popup.dart
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,18 @@ class LanguageToolMistakePopup extends StatelessWidget {
static const double _defaultVerticalMargin = 25.0;
static const double _defaultHorizontalMargin = 10.0;
static const double _defaultMaxWidth = 250.0;
static const _iconSize = 25.0;
static const double _logoSize = 25;
static const double _headerIconSize = 12;

static const double _borderRadius = 10.0;
static const double _mistakeNameFontSize = 11.0;
static const double _mistakeMessageFontSize = 13.0;
static const double _replacementButtonsSpacing = 4.0;
static const double _replacementButtonsSpacingMobile = -6.0;
static const double _paddingBetweenTitle = 14.0;
static const double _titleLetterSpacing = 0.56;
static const double _dismissSplashRadius = 2.0;
static const double _padding = 10.0;

/// Renderer used to display this window.
final PopupOverlayRenderer popupRenderer;
Expand Down Expand Up @@ -84,9 +95,11 @@ class LanguageToolMistakePopup extends StatelessWidget {
/// Mistake suggestion style.
final ButtonStyle? mistakeStyle;

/// [LanguageToolMistakePopup] constructor
/// Optional builder that adds additional actions to the header.
final Future<void> Function(String)? addWordToDictionary;

/// Creates a [LanguageToolMistakePopup].
const LanguageToolMistakePopup({
super.key,
required this.popupRenderer,
required this.mistake,
required this.controller,
Expand All @@ -96,21 +109,12 @@ class LanguageToolMistakePopup extends StatelessWidget {
this.horizontalMargin = _defaultHorizontalMargin,
this.verticalMargin = _defaultVerticalMargin,
this.mistakeStyle,
this.addWordToDictionary,
super.key,
});

@override
Widget build(BuildContext context) {
const _borderRadius = 10.0;
const _mistakeNameFontSize = 11.0;
const _mistakeMessageFontSize = 13.0;
const _replacementButtonsSpacing = 4.0;
const _replacementButtonsSpacingMobile = -6.0;
const _paddingBetweenTitle = 14.0;
const _titleLetterSpacing = 0.56;
const _dismissSplashRadius = 2.0;

const padding = 10.0;

final availableSpace = _calculateAvailableSpace(context);

final colorScheme = Theme.of(context).colorScheme;
Expand Down Expand Up @@ -149,43 +153,58 @@ class LanguageToolMistakePopup extends StatelessWidget {
children: [
Padding(
padding: const EdgeInsets.only(left: 4),
child: Row(
children: [
Expanded(
child: Row(
children: [
Padding(
padding: const EdgeInsets.only(right: 5.0),
child: Image.asset(
LangToolImages.logo,
width: _iconSize,
height: _iconSize,
package: 'languagetool_textfield',
child: IconTheme(
data: const IconThemeData(size: _headerIconSize),
child: Row(
children: [
Expanded(
child: Row(
children: [
Padding(
padding: const EdgeInsets.only(right: 5.0),
child: Image.asset(
LangToolImages.logo,
width: _logoSize,
height: _logoSize,
package: 'languagetool_textfield',
),
),
),
const Text('Correct'),
],
const Text('Correct'),
],
),
),
),
IconButton(
icon: const Icon(
Icons.close,
size: 12,
if (addWordToDictionary case final addWordToDictionary?)
IconButton(
icon: const Icon(Icons.menu_book),
constraints: const BoxConstraints(),
splashRadius: _dismissSplashRadius,
onPressed: () async {
final word = controller.text.substring(
mistake.offset,
mistake.endOffset,
);

await addWordToDictionary(word);

_fixTheMistake(word);
Comment on lines +181 to +189
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Should we extract this to the private method?

},
Comment thread
andrew-bekhiet-solid marked this conversation as resolved.
),
IconButton(
icon: const Icon(Icons.close),
constraints: const BoxConstraints(),
splashRadius: _dismissSplashRadius,
onPressed: () {
_dismissDialog();
controller.onClosePopup();
},
),
constraints: const BoxConstraints(),
padding: EdgeInsets.zero,
splashRadius: _dismissSplashRadius,
onPressed: () {
_dismissDialog();
controller.onClosePopup();
},
),
],
],
),
),
),
Container(
margin: const EdgeInsets.only(top: 8),
padding: const EdgeInsets.all(padding),
padding: const EdgeInsets.all(_padding),
decoration: BoxDecoration(
color: colorScheme.surface,
borderRadius: BorderRadius.circular(_borderRadius),
Expand All @@ -210,7 +229,7 @@ class LanguageToolMistakePopup extends StatelessWidget {
),
),
Padding(
padding: const EdgeInsets.only(bottom: padding),
padding: const EdgeInsets.only(bottom: _padding),
child: Text(
mistake.message,
style: const TextStyle(
Expand Down
Loading
Loading