From c368843c9f2903d0799fe46f1ef380dda8941ce5 Mon Sep 17 00:00:00 2001 From: Andrew Bekhiet Date: Tue, 9 Sep 2025 15:22:27 +0300 Subject: [PATCH 1/8] style: run dart formatter --- lib/src/utils/mistake_popup.dart | 38 ++++++++++++++++---------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/lib/src/utils/mistake_popup.dart b/lib/src/utils/mistake_popup.dart index 89463b1..fd30741 100644 --- a/lib/src/utils/mistake_popup.dart +++ b/lib/src/utils/mistake_popup.dart @@ -86,11 +86,11 @@ class LanguageToolMistakePopup extends StatelessWidget { /// [LanguageToolMistakePopup] constructor const LanguageToolMistakePopup({ - super.key, required this.popupRenderer, required this.mistake, required this.controller, required this.mistakePosition, + super.key, this.maxWidth = _defaultMaxWidth, this.maxHeight = double.infinity, this.horizontalMargin = _defaultHorizontalMargin, @@ -100,14 +100,14 @@ class LanguageToolMistakePopup extends StatelessWidget { @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 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; @@ -128,7 +128,7 @@ class LanguageToolMistakePopup extends StatelessWidget { ), decoration: BoxDecoration( color: colorScheme.surface.withValues(alpha: 0.9), - borderRadius: BorderRadius.circular(_borderRadius), + borderRadius: BorderRadius.circular(borderRadius), boxShadow: [ BoxShadow( color: colorScheme.onSurface.withValues(alpha: 0.5), @@ -174,7 +174,7 @@ class LanguageToolMistakePopup extends StatelessWidget { ), constraints: const BoxConstraints(), padding: EdgeInsets.zero, - splashRadius: _dismissSplashRadius, + splashRadius: dismissSplashRadius, onPressed: () { _dismissDialog(); controller.onClosePopup(); @@ -188,7 +188,7 @@ class LanguageToolMistakePopup extends StatelessWidget { padding: const EdgeInsets.all(padding), decoration: BoxDecoration( color: colorScheme.surface, - borderRadius: BorderRadius.circular(_borderRadius), + borderRadius: BorderRadius.circular(borderRadius), ), child: SingleChildScrollView( child: Column( @@ -196,16 +196,16 @@ class LanguageToolMistakePopup extends StatelessWidget { children: [ Padding( padding: const EdgeInsets.only( - bottom: _paddingBetweenTitle, + bottom: paddingBetweenTitle, ), child: Text( mistake.type.name.capitalize(), style: TextStyle( color: colorScheme.onSurface.withValues(alpha: 0.7), - fontSize: _mistakeNameFontSize, + fontSize: mistakeNameFontSize, fontWeight: FontWeight.w600, - letterSpacing: _titleLetterSpacing, + letterSpacing: titleLetterSpacing, ), ), ), @@ -214,15 +214,15 @@ class LanguageToolMistakePopup extends StatelessWidget { child: Text( mistake.message, style: const TextStyle( - fontSize: _mistakeMessageFontSize, + fontSize: mistakeMessageFontSize, ), ), ), Wrap( - spacing: _replacementButtonsSpacing, + spacing: replacementButtonsSpacing, runSpacing: kIsWeb - ? _replacementButtonsSpacing - : _replacementButtonsSpacingMobile, + ? replacementButtonsSpacing + : replacementButtonsSpacingMobile, children: mistake.replacements .map( (replacement) => ElevatedButton( From c376550699a5d270731d859aef2b1b0481f0f30a Mon Sep 17 00:00:00 2001 From: Andrew Bekhiet Date: Tue, 9 Sep 2025 15:49:54 +0300 Subject: [PATCH 2/8] feat: add actions builder to allow adding actions --- lib/src/utils/mistake_popup.dart | 67 +++++++++++++++++--------------- 1 file changed, 36 insertions(+), 31 deletions(-) diff --git a/lib/src/utils/mistake_popup.dart b/lib/src/utils/mistake_popup.dart index fd30741..9c1693e 100644 --- a/lib/src/utils/mistake_popup.dart +++ b/lib/src/utils/mistake_popup.dart @@ -84,18 +84,22 @@ class LanguageToolMistakePopup extends StatelessWidget { /// Mistake suggestion style. final ButtonStyle? mistakeStyle; - /// [LanguageToolMistakePopup] constructor + /// Optional builder that adds additional actions to the header. + final List Function(BuildContext context)? actionsBuilder; + + /// Creates a [LanguageToolMistakePopup]. const LanguageToolMistakePopup({ required this.popupRenderer, required this.mistake, required this.controller, required this.mistakePosition, - super.key, this.maxWidth = _defaultMaxWidth, this.maxHeight = double.infinity, this.horizontalMargin = _defaultHorizontalMargin, this.verticalMargin = _defaultVerticalMargin, this.mistakeStyle, + this.actionsBuilder, + super.key, }); @override @@ -149,38 +153,39 @@ 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: 12), + 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', + ), ), - ), - const Text('Correct'), - ], + const Text('Correct'), + ], + ), ), - ), - IconButton( - icon: const Icon( - Icons.close, - size: 12, + ...?actionsBuilder?.call(context), + IconButton( + icon: const Icon(Icons.close), + constraints: const BoxConstraints(), + padding: EdgeInsets.zero, + splashRadius: dismissSplashRadius, + onPressed: () { + _dismissDialog(); + controller.onClosePopup(); + }, ), - constraints: const BoxConstraints(), - padding: EdgeInsets.zero, - splashRadius: dismissSplashRadius, - onPressed: () { - _dismissDialog(); - controller.onClosePopup(); - }, - ), - ], + ], + ), ), ), Container( From 3708476baa70f12700e3e7fd77a5f0538f13ec41 Mon Sep 17 00:00:00 2001 From: Andrew Bekhiet Date: Tue, 9 Sep 2025 15:59:47 +0300 Subject: [PATCH 3/8] refactor: avoid magic numbers style: move all constants to class static members --- lib/src/utils/mistake_popup.dart | 54 ++++++++++++++++---------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/lib/src/utils/mistake_popup.dart b/lib/src/utils/mistake_popup.dart index 9c1693e..22ae41f 100644 --- a/lib/src/utils/mistake_popup.dart +++ b/lib/src/utils/mistake_popup.dart @@ -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; @@ -104,17 +115,6 @@ class LanguageToolMistakePopup extends StatelessWidget { @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; @@ -132,7 +132,7 @@ class LanguageToolMistakePopup extends StatelessWidget { ), decoration: BoxDecoration( color: colorScheme.surface.withValues(alpha: 0.9), - borderRadius: BorderRadius.circular(borderRadius), + borderRadius: BorderRadius.circular(_borderRadius), boxShadow: [ BoxShadow( color: colorScheme.onSurface.withValues(alpha: 0.5), @@ -154,7 +154,7 @@ class LanguageToolMistakePopup extends StatelessWidget { Padding( padding: const EdgeInsets.only(left: 4), child: IconTheme( - data: const IconThemeData(size: 12), + data: const IconThemeData(size: _headerIconSize), child: Row( children: [ Expanded( @@ -164,8 +164,8 @@ class LanguageToolMistakePopup extends StatelessWidget { padding: const EdgeInsets.only(right: 5.0), child: Image.asset( LangToolImages.logo, - width: _iconSize, - height: _iconSize, + width: _logoSize, + height: _logoSize, package: 'languagetool_textfield', ), ), @@ -178,7 +178,7 @@ class LanguageToolMistakePopup extends StatelessWidget { icon: const Icon(Icons.close), constraints: const BoxConstraints(), padding: EdgeInsets.zero, - splashRadius: dismissSplashRadius, + splashRadius: _dismissSplashRadius, onPressed: () { _dismissDialog(); controller.onClosePopup(); @@ -190,10 +190,10 @@ class LanguageToolMistakePopup extends StatelessWidget { ), 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), + borderRadius: BorderRadius.circular(_borderRadius), ), child: SingleChildScrollView( child: Column( @@ -201,33 +201,33 @@ class LanguageToolMistakePopup extends StatelessWidget { children: [ Padding( padding: const EdgeInsets.only( - bottom: paddingBetweenTitle, + bottom: _paddingBetweenTitle, ), child: Text( mistake.type.name.capitalize(), style: TextStyle( color: colorScheme.onSurface.withValues(alpha: 0.7), - fontSize: mistakeNameFontSize, + fontSize: _mistakeNameFontSize, fontWeight: FontWeight.w600, - letterSpacing: titleLetterSpacing, + letterSpacing: _titleLetterSpacing, ), ), ), Padding( - padding: const EdgeInsets.only(bottom: padding), + padding: const EdgeInsets.only(bottom: _padding), child: Text( mistake.message, style: const TextStyle( - fontSize: mistakeMessageFontSize, + fontSize: _mistakeMessageFontSize, ), ), ), Wrap( - spacing: replacementButtonsSpacing, + spacing: _replacementButtonsSpacing, runSpacing: kIsWeb - ? replacementButtonsSpacing - : replacementButtonsSpacingMobile, + ? _replacementButtonsSpacing + : _replacementButtonsSpacingMobile, children: mistake.replacements .map( (replacement) => ElevatedButton( From a6e9a6504c84545ce638fae79df2f99b1e02548c Mon Sep 17 00:00:00 2001 From: Andrew Bekhiet Date: Wed, 10 Sep 2025 17:28:34 +0300 Subject: [PATCH 4/8] feat: add "add to dictionary" feature ui: use default icon button padding export result to allow custom language check service implementations --- example/lib/main.dart | 163 +++++++++++++++--- lib/languagetool_textfield.dart | 2 + .../controllers/language_tool_controller.dart | 12 +- lib/src/utils/mistake_popup.dart | 22 ++- ...ory_dictionary_language_check_service.dart | 51 ++++++ 5 files changed, 221 insertions(+), 29 deletions(-) create mode 100644 lib/src/wrappers/in_memory_dictionary_language_check_service.dart diff --git a/example/lib/main.dart b/example/lib/main.dart index 968adf1..bfd3b5c 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -20,45 +20,116 @@ class App extends StatefulWidget { } class _AppState extends State { - /// Initialize LanguageToolController - final LanguageToolController _controller = LanguageToolController(); + Set _dictionary = {}; + final _addWordController = TextEditingController(); - static const List 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( - 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), + ), + ), + ], + ), + ), ), ], ), @@ -66,9 +137,53 @@ class _AppState extends State { ); } + 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(); } } diff --git a/lib/languagetool_textfield.dart b/lib/languagetool_textfield.dart index fa751af..b7e5828 100644 --- a/lib/languagetool_textfield.dart +++ b/lib/languagetool_textfield.dart @@ -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'; diff --git a/lib/src/core/controllers/language_tool_controller.dart b/lib/src/core/controllers/language_tool_controller.dart index 40867a4..b810def 100644 --- a/lib/src/core/controllers/language_tool_controller.dart +++ b/lib/src/core/controllers/language_tool_controller.dart @@ -59,7 +59,7 @@ class LanguageToolController extends TextEditingController { _isEnabled = value; if (_isEnabled) { - _handleTextChange(text, spellCheckSameText: true); + recheckText(); } else { _mistakes = []; for (final recognizer in _recognizers) { @@ -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 _handleTextChange( diff --git a/lib/src/utils/mistake_popup.dart b/lib/src/utils/mistake_popup.dart index 22ae41f..a4595d3 100644 --- a/lib/src/utils/mistake_popup.dart +++ b/lib/src/utils/mistake_popup.dart @@ -96,7 +96,7 @@ class LanguageToolMistakePopup extends StatelessWidget { final ButtonStyle? mistakeStyle; /// Optional builder that adds additional actions to the header. - final List Function(BuildContext context)? actionsBuilder; + final Future Function(String)? addWordToDictionary; /// Creates a [LanguageToolMistakePopup]. const LanguageToolMistakePopup({ @@ -109,7 +109,7 @@ class LanguageToolMistakePopup extends StatelessWidget { this.horizontalMargin = _defaultHorizontalMargin, this.verticalMargin = _defaultVerticalMargin, this.mistakeStyle, - this.actionsBuilder, + this.addWordToDictionary, super.key, }); @@ -173,11 +173,25 @@ class LanguageToolMistakePopup extends StatelessWidget { ], ), ), - ...?actionsBuilder?.call(context), + 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); + }, + ), IconButton( icon: const Icon(Icons.close), constraints: const BoxConstraints(), - padding: EdgeInsets.zero, splashRadius: _dismissSplashRadius, onPressed: () { _dismissDialog(); diff --git a/lib/src/wrappers/in_memory_dictionary_language_check_service.dart b/lib/src/wrappers/in_memory_dictionary_language_check_service.dart new file mode 100644 index 0000000..d56b534 --- /dev/null +++ b/lib/src/wrappers/in_memory_dictionary_language_check_service.dart @@ -0,0 +1,51 @@ +import 'package:languagetool_textfield/languagetool_textfield.dart'; + +/// A language-check service that filters LanguageTool suggestions using an in-memory dictionary. +/// +/// This class wraps a LanguageToolService and extends ThrottlingLanguageCheckService to +/// limit the frequency of requests. After performing a check with the underlying service, +/// it removes any reported mistakes whose corresponding word is present in the provided +/// in-memory dictionary (so user-defined or domain-specific words can be treated as correct). +/// +/// The filtering is performed by extracting the substring of the input text using each +/// mistake's offset and endOffset and checking membership against the dictionary returned +/// by [getDictionary]. +/// +/// Note: the underlying service is throttled to avoid excessive requests; the throttling +/// behavior is provided by the superclass. +class InMemoryDictionaryLanguageCheckService + extends ThrottlingLanguageCheckService { + /// Callback that supplies the current set of words to be treated as correct. + /// + /// This function is invoked for each text check so the dictionary can be dynamic + /// (for example, reflecting user edits or settings). It must return a Set + /// containing the words that should be ignored by the language checker. + final Set Function() getDictionary; + + /// Creates an InMemoryDictionaryLanguageCheckService that uses [getDictionary] to filter mistakes. + /// + /// The [getDictionary] callback is required and will be called for every check operation. + /// The service delegates checking to an internal LanguageToolService and then filters + /// the results based on the returned dictionary. + InMemoryDictionaryLanguageCheckService({required this.getDictionary}) + : super( + LanguageToolService(LanguageToolClient()), + const Duration(milliseconds: 250), + ); + + @override + Future>?> findMistakes(String text) async { + final result = await super.findMistakes(text); + final dictionary = getDictionary(); + + return result?.map( + (mistakes) => mistakes.where( + (mistake) { + final word = text.substring(mistake.offset, mistake.endOffset); + + return !dictionary.contains(word); + }, + ).toList(), + ); + } +} From 85dbe56eeef2cb3f10b2cb16b76ad73e3ba05946 Mon Sep 17 00:00:00 2001 From: Andrew Bekhiet Date: Wed, 10 Sep 2025 17:32:36 +0300 Subject: [PATCH 5/8] doc: update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c345970..0a66c8b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 From 3e4a01177540a05d87fba7563bc2de4d593b4deb Mon Sep 17 00:00:00 2001 From: Andrew Bekhiet Date: Wed, 10 Sep 2025 18:09:56 +0300 Subject: [PATCH 6/8] fix: add scroll view to scroll the ui --- example/lib/main.dart | 169 +++++++++++++++++++++--------------------- 1 file changed, 85 insertions(+), 84 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index bfd3b5c..e07566d 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -39,99 +39,100 @@ class _AppState extends State { return Material( child: Scaffold( - body: Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - LanguageToolTextField( - controller: spellCheckController, - language: 'en-US', - mistakePopup: MistakePopup( - popupRenderer: PopupOverlayRenderer(), - mistakeBuilder: _mistakeBuilder, + body: SingleChildScrollView( + child: Column( + children: [ + LanguageToolTextField( + controller: spellCheckController, + language: 'en-US', + mistakePopup: MistakePopup( + popupRenderer: PopupOverlayRenderer(), + mistakeBuilder: _mistakeBuilder, + ), ), - ), - ValueListenableBuilder( - valueListenable: spellCheckController, - builder: (_, __, ___) => CheckboxListTile( - title: const Text("Enable spell checking"), - value: spellCheckController.isEnabled, - onChanged: (value) => - spellCheckController.isEnabled = value ?? false, + ValueListenableBuilder( + valueListenable: spellCheckController, + builder: (_, __, ___) => CheckboxListTile( + title: const Text("Enable spell checking"), + value: spellCheckController.isEnabled, + onChanged: (value) => + spellCheckController.isEnabled = value ?? false, + ), ), - ), - 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: 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(), + const SizedBox(height: 16), + Row( + children: [ + Expanded( + child: TextField( + controller: _addWordController, + decoration: const InputDecoration( + labelText: 'Add word to dictionary', + border: OutlineInputBorder(), + ), + onSubmitted: (_) => _addWord(), ), - 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(width: 8), + ElevatedButton( + onPressed: _addWord, + child: const Text('Add'), ), - ], - ), - 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), + ], + ), + 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), + ), + ), + ], + ), ), ), - ), - ], + ], + ), ), ), ); From c2da5b63387a893f4c67bfd27a114253ef8a41c0 Mon Sep 17 00:00:00 2001 From: Andrew Bekhiet Date: Wed, 10 Sep 2025 18:10:20 +0300 Subject: [PATCH 7/8] reactor: allow changing language tool service and throttling duration --- .../in_memory_dictionary_language_check_service.dart | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/lib/src/wrappers/in_memory_dictionary_language_check_service.dart b/lib/src/wrappers/in_memory_dictionary_language_check_service.dart index d56b534..871bfa2 100644 --- a/lib/src/wrappers/in_memory_dictionary_language_check_service.dart +++ b/lib/src/wrappers/in_memory_dictionary_language_check_service.dart @@ -27,10 +27,13 @@ class InMemoryDictionaryLanguageCheckService /// The [getDictionary] callback is required and will be called for every check operation. /// The service delegates checking to an internal LanguageToolService and then filters /// the results based on the returned dictionary. - InMemoryDictionaryLanguageCheckService({required this.getDictionary}) - : super( - LanguageToolService(LanguageToolClient()), - const Duration(milliseconds: 250), + InMemoryDictionaryLanguageCheckService({ + required this.getDictionary, + LanguageCheckService? languageToolService, + Duration? throttlingDuration, + }) : super( + languageToolService ?? LanguageToolService(LanguageToolClient()), + throttlingDuration ?? const Duration(milliseconds: 250), ); @override From d2d16501e2e5fcc9c05054f5c9a541a15c794a67 Mon Sep 17 00:00:00 2001 From: Andrew Bekhiet Date: Wed, 10 Sep 2025 18:38:49 +0300 Subject: [PATCH 8/8] refactor: avoid widget returning methods --- example/lib/main.dart | 34 ++++++++++++++++------------------ 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index e07566d..90dd867 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -47,7 +47,22 @@ class _AppState extends State { language: 'en-US', mistakePopup: MistakePopup( popupRenderer: PopupOverlayRenderer(), - mistakeBuilder: _mistakeBuilder, + 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}); + }, + ); + }, ), ), ValueListenableBuilder( @@ -164,23 +179,6 @@ class _AppState extends State { }); } - 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() { _spellCheckController?.dispose();