-
Notifications
You must be signed in to change notification settings - Fork 7
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
Suggestions popup #28
Closed
Closed
Changes from all commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
4b8241a
Basic widget to display replacements for a mistake.
Sammy275 f6f8ebd
Updated the feature now it has an overlay instead of a dialog
Sammy275 f260d84
Visual fixes,only 4 or less suggestions will appear, added documentation
SaimIrfan bf6d6be
Restructuringof the pop up widget
SaimIrfan c94d145
Removed magic value and added static default suggestions limit
SaimIrfan 47c6790
Added the functionality of closing the suggestions pop up
SaimIrfan 0e28900
Updated the formating and documentation
SaimIrfan 462cc11
Added logic to calculate the x axis of the popup so it does not excee…
SaimIrfan 4185617
Updated documentation
SaimIrfan 2103afb
Removed magic values
Sammy275 df7afca
Removed non null assertion
Sammy275 6a6c12d
Added a getter in Mistake class to get the name. Resolved the non nul…
Sammy275 bf6999d
The suggestion popup's design can now be added from outside.
Sammy275 b03ba4b
Fixed formatiing of the code
Sammy275 fd7b2d3
The text box will now appear right beneath the mistake instead being …
Sammy275 7501bdd
Removing the overlay when the controller gets disposed
Sammy275 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,12 +1,25 @@ | ||
import 'package:flutter/material.dart'; | ||
import 'package:flutter/gestures.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'; | ||
import 'package:languagetool_textfield/presentation/suggestions_popup.dart'; | ||
|
||
/// A TextEditingController with overrides buildTextSpan for building | ||
/// marked TextSpans with tap recognizer | ||
class ColoredTextEditingController extends TextEditingController { | ||
/// An [OverlayEntry] for the [Overlay] class. | ||
/// This entry represents the floating popup | ||
/// that appears on a mistake. | ||
OverlayEntry? _overlayEntry; | ||
|
||
/// Represents the maximum numbers of suggestions allowed. | ||
final int suggestionsLimit; | ||
|
||
static const int _defaultSuggestionLimit = 4; | ||
|
||
/// Color scheme to highlight mistakes | ||
final HighlightStyle highlightStyle; | ||
|
||
|
@@ -16,6 +29,17 @@ class ColoredTextEditingController extends TextEditingController { | |
/// List which contains Mistake objects spans are built from | ||
List<Mistake> _mistakes = []; | ||
|
||
/// A builder function to build a custom mistake widget. | ||
/// If it is not provided then a default widget will be displayed. | ||
final Widget Function( | ||
String name, | ||
String message, | ||
Color color, | ||
List<String> replacements, | ||
Function(String) onSuggestionTap, | ||
Function() onClose, | ||
)? mistakeBuilder; | ||
|
||
@override | ||
set value(TextEditingValue newValue) { | ||
_handleTextChange(newValue.text); | ||
|
@@ -25,21 +49,33 @@ class ColoredTextEditingController extends TextEditingController { | |
/// Controller constructor | ||
ColoredTextEditingController({ | ||
required this.languageCheckService, | ||
this.mistakeBuilder, | ||
this.highlightStyle = const HighlightStyle(), | ||
this.suggestionsLimit = _defaultSuggestionLimit, | ||
}); | ||
|
||
/// 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; | ||
if (newText == text) return; | ||
_mistakes.clear(); | ||
final mistakes = await languageCheckService.findMistakes(newText); | ||
if (mistakes.isNotEmpty) { | ||
_mistakes = mistakes; | ||
notifyListeners(); | ||
} | ||
|
||
/// To remove the overlay when new text is entered in the field. | ||
_removeHighlightOverlay(); | ||
} | ||
|
||
@override | ||
void dispose() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Move this to the bottom of the class. |
||
/// Removing the overlay when the controller gets disposed. | ||
_removeHighlightOverlay(); | ||
super.dispose(); | ||
} | ||
|
||
/// Generates TextSpan from Mistake list | ||
|
@@ -50,6 +86,7 @@ class ColoredTextEditingController extends TextEditingController { | |
required bool withComposing, | ||
}) { | ||
final formattedTextSpans = _generateSpans( | ||
context: context, | ||
style: style, | ||
); | ||
|
||
|
@@ -58,8 +95,27 @@ class ColoredTextEditingController extends TextEditingController { | |
); | ||
} | ||
|
||
Widget _widgetBuilder( | ||
String name, | ||
String message, | ||
Color color, | ||
List<String> replacements, | ||
void Function(String) onSuggestionTap, | ||
void Function() onClose, | ||
) { | ||
return SuggestionsPopup( | ||
mistakeName: name, | ||
mistakeMessage: message, | ||
mistakeColor: color, | ||
replacements: replacements, | ||
onTapCallback: onSuggestionTap, | ||
closeCallBack: onClose, | ||
); | ||
} | ||
|
||
/// Generator function to create TextSpan instances | ||
Iterable<TextSpan> _generateSpans({ | ||
required BuildContext context, | ||
TextStyle? style, | ||
}) sync* { | ||
int currentOffset = 0; // enter index | ||
|
@@ -77,10 +133,73 @@ class ColoredTextEditingController extends TextEditingController { | |
/// Get a highlight color | ||
final Color mistakeColor = _getMistakeColor(mistake.type); | ||
|
||
/// Number of suggestions depends on a limit. | ||
/// By default it is being set in _defaultSuggestionLimit | ||
final List<String> replacements = | ||
mistake.replacements.length <= suggestionsLimit | ||
? mistake.replacements | ||
: mistake.replacements.sublist(0, suggestionsLimit); | ||
|
||
/// Parsing the mistake enum types to string type | ||
final String mistakeName = mistake.name; | ||
|
||
/// Mistake highlighted TextSpan | ||
yield TextSpan( | ||
children: [ | ||
TextSpan( | ||
recognizer: TapGestureRecognizer() | ||
..onTapDown = (TapDownDetails pressDetails) { | ||
/// getting the position of the user's finger | ||
final position = pressDetails.globalPosition; | ||
final screenWidth = MediaQuery.of(context).size.width; | ||
final containerWidth = screenWidth / 1.5; | ||
|
||
final newDx = _calculateDxOfPopup( | ||
position.dx, | ||
screenWidth, | ||
containerWidth, | ||
); | ||
|
||
final newDy = _calculateDyOfPopup(context, position.dy); | ||
|
||
/// To remove overlay if present. | ||
_removeHighlightOverlay(); | ||
|
||
final callback = mistakeBuilder ?? _widgetBuilder; | ||
|
||
final overlayEntry = OverlayEntry( | ||
builder: (BuildContext context) { | ||
return Positioned( | ||
top: newDy, | ||
left: newDx, | ||
child: Material( | ||
type: MaterialType.transparency, | ||
child: SizedBox( | ||
width: containerWidth, | ||
child: callback( | ||
mistakeName, | ||
mistake.message, | ||
mistakeColor, | ||
replacements, | ||
(newValue) { | ||
text = text.replaceRange( | ||
mistake.offset, | ||
mistake.offset + mistake.length, | ||
newValue, | ||
); | ||
_removeHighlightOverlay(); | ||
}, | ||
_removeHighlightOverlay, | ||
), | ||
), | ||
), | ||
); | ||
}, | ||
); | ||
|
||
_overlayEntry = overlayEntry; | ||
Overlay.of(context).insert(overlayEntry); | ||
}, | ||
text: | ||
text.substring(mistake.offset, mistake.offset + mistake.length), | ||
mouseCursor: MaterialStateMouseCursor.clickable, | ||
|
@@ -106,6 +225,11 @@ class ColoredTextEditingController extends TextEditingController { | |
); | ||
} | ||
|
||
void _removeHighlightOverlay() { | ||
_overlayEntry?.remove(); | ||
_overlayEntry = null; | ||
} | ||
|
||
/// Returns color for mistake TextSpan style | ||
Color _getMistakeColor(MistakeType type) { | ||
switch (type) { | ||
|
@@ -125,4 +249,42 @@ class ColoredTextEditingController extends TextEditingController { | |
return highlightStyle.otherMistakeColor; | ||
} | ||
} | ||
|
||
/// Calculates the new x axis for the popup so that it wont exceed the screen. | ||
double _calculateDxOfPopup( | ||
double dxOfTap, | ||
double screenWidth, | ||
double containerWidth, | ||
) { | ||
/// The x axis point after which the popup exceeds the screen | ||
final dxBoundary = screenWidth - containerWidth; | ||
|
||
/// Calculating final x axis | ||
final newDx = dxOfTap >= dxBoundary ? dxBoundary : dxOfTap; | ||
|
||
return newDx; | ||
} | ||
|
||
/// Calculates y axis for the popup so that it appears right below the mistake | ||
double _calculateDyOfPopup(BuildContext context, double originalDy) { | ||
/// getting the rendering information of the TextSpan widget | ||
final textSpanRenderBox = context.findRenderObject() as RenderBox?; | ||
|
||
/// This will be the default y axis | ||
/// if the rendering information is not available | ||
var newDy = originalDy + 10; | ||
|
||
if (textSpanRenderBox != null) { | ||
/// Top-Left corner of the TextSpan widget | ||
final offset = textSpanRenderBox.localToGlobal(Offset.zero); | ||
|
||
final textSpanHeight = textSpanRenderBox.size.height; | ||
|
||
/// Adding the pixels in the height of text span to | ||
/// the top left corner point of text span | ||
newDy = offset.dy + textSpanHeight; | ||
} | ||
|
||
return newDy; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
import 'package:flutter/material.dart'; | ||
|
||
/// A [StatelessWidget] that is inserted in the [Overlay]. | ||
/// This widget represents the pop up that appears when a mistake is present in | ||
/// the text. | ||
class SuggestionsPopup extends StatelessWidget { | ||
/// represents the [Color] of the mistake for which | ||
/// the [SuggestionsPopup] is being displayed. | ||
final Color mistakeColor; | ||
|
||
/// represents the name of the mistake. | ||
final String mistakeName; | ||
|
||
/// The description of the mistake. | ||
final String mistakeMessage; | ||
|
||
/// List that contains the replacement for the given mistake | ||
final List<String> replacements; | ||
|
||
/// A callback that will run when a suggestion is pressed. | ||
final void Function(String suggestion) onTapCallback; | ||
|
||
/// A callback function that will pop [SuggestionsPopup] from the [Overlay] | ||
final void Function() closeCallBack; | ||
|
||
static const double _mistakeCircleSize = 10.0; | ||
static const double _iconSize = 20.0; | ||
|
||
/// Constructor for the [SuggestionsPopup] widget. | ||
const SuggestionsPopup({ | ||
required this.mistakeName, | ||
required this.mistakeMessage, | ||
required this.mistakeColor, | ||
required this.replacements, | ||
required this.onTapCallback, | ||
required this.closeCallBack, | ||
Key? key, | ||
}) : super(key: key); | ||
|
||
@override | ||
Widget build(BuildContext context) { | ||
return Card( | ||
child: Container( | ||
padding: const EdgeInsets.all(10.0), | ||
child: Column( | ||
mainAxisSize: MainAxisSize.min, | ||
crossAxisAlignment: CrossAxisAlignment.start, | ||
children: [ | ||
Row( | ||
children: [ | ||
Container( | ||
width: _mistakeCircleSize, | ||
height: _mistakeCircleSize, | ||
decoration: BoxDecoration( | ||
shape: BoxShape.circle, | ||
color: mistakeColor, | ||
), | ||
), | ||
const SizedBox(width: 5.0), | ||
Expanded( | ||
child: Text( | ||
mistakeName, | ||
style: const TextStyle( | ||
fontWeight: FontWeight.bold, | ||
), | ||
), | ||
), | ||
IconButton( | ||
icon: const Icon(Icons.close), | ||
iconSize: _iconSize, | ||
padding: EdgeInsets.zero, | ||
constraints: const BoxConstraints(), | ||
onPressed: closeCallBack, | ||
), | ||
], | ||
), | ||
const SizedBox(height: 20.0), | ||
Text( | ||
mistakeMessage, | ||
softWrap: true, | ||
), | ||
const SizedBox(height: 20.0), | ||
Wrap( | ||
children: replacements | ||
.map( | ||
(elem) => GestureDetector( | ||
onTap: () { | ||
onTapCallback(elem); | ||
}, | ||
child: Container( | ||
margin: const EdgeInsets.only( | ||
right: 5.0, | ||
bottom: 5.0, | ||
), | ||
decoration: const BoxDecoration( | ||
color: Colors.lightBlue, | ||
borderRadius: BorderRadius.all( | ||
Radius.circular(5.0), | ||
), | ||
), | ||
padding: const EdgeInsets.all(8.0), | ||
child: Text( | ||
elem, | ||
style: const TextStyle( | ||
color: Colors.white, | ||
fontWeight: FontWeight.bold, | ||
), | ||
), | ||
), | ||
), | ||
) | ||
.toList(), | ||
), | ||
], | ||
), | ||
), | ||
); | ||
} | ||
} |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.