Issue
I have a spell checker demo here, visually it is exactly what I want (red underline for words that are not correct), but I'm having trouble creating a right-click context menu to apply suggestions.
I was able to get a context menu on the Text
object, but I was not able to find the position of the text in the box to replace using the prediction.
Here is the code:
pom.xml
<dependency>
<groupId>org.fxmisc.richtext</groupId>
<artifactId>richtextfx</artifactId>
<version>0.10.6</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-text</artifactId>
<version>1.9</version>
<type>jar</type>
</dependency>
SpellCheckDemo.java
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.text.BreakIterator;
import java.time.Duration;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import org.fxmisc.flowless.VirtualizedScrollPane;
import org.fxmisc.richtext.StyleClassedTextArea;
import org.fxmisc.richtext.model.StyleSpans;
import org.fxmisc.richtext.model.StyleSpansBuilder;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.MenuItem;
import javafx.scene.input.ContextMenuEvent;
import javafx.scene.layout.StackPane;
import javafx.scene.text.Text;
import javafx.stage.Stage;
import org.apache.commons.text.similarity.JaroWinklerDistance;
import org.reactfx.Subscription;
public class SpellCheckingDemo extends Application
{
private static final Set<String> dictionary = new HashSet<String>();
private final static double JAROWINKLERDISTANCE_THRESHOLD = .80;
public static void main(String[] args)
{
launch(args);
}
@Override
public void start(Stage primaryStage)
{
StyleClassedTextArea textArea = new StyleClassedTextArea();
textArea.setWrapText(true);
Subscription cleanupWhenFinished = textArea.multiPlainChanges()
.successionEnds(Duration.ofMillis(500))
.subscribe(change ->
{
textArea.setStyleSpans(0, computeHighlighting(textArea.getText()));
});
// call when no longer need it: `cleanupWhenFinished.unsubscribe();`
textArea.setOnContextMenuRequested((ContextMenuEvent event) ->
{
if (event.getTarget() instanceof Text)
{
Text text = (Text) event.getTarget();
ContextMenu context = new ContextMenu();
JaroWinklerDistance distance = new JaroWinklerDistance();
for (String word : dictionary)
{
if (distance.apply(text.getText(), word) >= JAROWINKLERDISTANCE_THRESHOLD)
{
MenuItem item = new MenuItem(word);
item.setOnAction(a ->
{
// how do I find the position of the Text object ?
textArea.replaceText(25, 25 + text.getText().length(), word);
});
context.getItems().add(item);
}
}
context.show(primaryStage, event.getScreenX(), event.getScreenY());
}
});
// load the dictionary
try (InputStream input = SpellCheckingDemo.class.getResourceAsStream("/spellchecking.dict");
BufferedReader br = new BufferedReader(new InputStreamReader(input)))
{
String line;
while ((line = br.readLine()) != null)
{
dictionary.add(line);
}
} catch (IOException e)
{
e.printStackTrace();
}
// load the sample document
InputStream input2 = SpellCheckingDemo.class.getResourceAsStream("/spellchecking.txt");
try (java.util.Scanner s = new java.util.Scanner(input2))
{
String document = s.useDelimiter("\\A").hasNext() ? s.next() : "";
textArea.replaceText(0, 0, document);
}
Scene scene = new Scene(new StackPane(new VirtualizedScrollPane<>(textArea)), 600, 400);
scene.getStylesheets().add(SpellCheckingDemo.class.getResource("/spellchecking.css").toExternalForm());
primaryStage.setScene(scene);
primaryStage.setTitle("Spell Checking Demo");
primaryStage.show();
}
private static StyleSpans<Collection<String>> computeHighlighting(String text)
{
StyleSpansBuilder<Collection<String>> spansBuilder = new StyleSpansBuilder<>();
BreakIterator wb = BreakIterator.getWordInstance();
wb.setText(text);
int lastIndex = wb.first();
int lastKwEnd = 0;
while (lastIndex != BreakIterator.DONE)
{
int firstIndex = lastIndex;
lastIndex = wb.next();
if (lastIndex != BreakIterator.DONE
&& Character.isLetterOrDigit(text.charAt(firstIndex)))
{
String word = text.substring(firstIndex, lastIndex).toLowerCase();
if (!dictionary.contains(word))
{
spansBuilder.add(Collections.emptyList(), firstIndex - lastKwEnd);
spansBuilder.add(Collections.singleton("underlined"), lastIndex - firstIndex);
lastKwEnd = lastIndex;
}
System.err.println();
}
}
spansBuilder.add(Collections.emptyList(), text.length() - lastKwEnd);
return spansBuilder.create();
}
}
The following files go into the resource folder:
spellchecking.css
.underlined {
-rtfx-background-color: #f0f0f0;
-rtfx-underline-color: red;
-rtfx-underline-dash-array: 2 2;
-rtfx-underline-width: 1;
-rtfx-underline-cap: butt;
}
spellchecking.dict
a
applied
basic
brown
but
could
document
dog
fox
here
if
is
its
jumps
lazy
no
over
quick
rendering
sample
see
styling
the
there
this
were
you
spellchecking.txt
The quik brown fox jumps over the lazy dog.
Ths is a sample dokument.
There is no styling aplied, but if there were, you could see its basic rndering here.
Solution
I found out how. By using the caret position, I can select a word and replace it. The problem is, right clicking didn't move the caret. So in order to move the caret, you add a listener.
textArea.setOnMouseClicked((MouseEvent mouseEvent) ->
{
if (mouseEvent.getButton().equals(MouseButton.SECONDARY))
{
if (mouseEvent.getClickCount() == 1)
{
CharacterHit hit = textArea.hit(mouseEvent.getX(), mouseEvent.getY());
int characterPosition = hit.getInsertionIndex();
// move the caret to that character's position
textArea.moveTo(characterPosition, SelectionPolicy.CLEAR);
}
}
});
Full code:
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.text.BreakIterator;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.fxmisc.flowless.VirtualizedScrollPane;
import org.fxmisc.richtext.StyleClassedTextArea;
import org.fxmisc.richtext.model.StyleSpans;
import org.fxmisc.richtext.model.StyleSpansBuilder;
import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.scene.Scene;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.MenuItem;
import javafx.scene.input.ContextMenuEvent;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.StackPane;
import javafx.scene.text.Text;
import javafx.stage.Stage;
import org.apache.commons.lang3.CharUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.text.WordUtils;
import org.apache.commons.text.similarity.JaroWinklerDistance;
import org.fxmisc.richtext.CharacterHit;
import org.fxmisc.richtext.NavigationActions.SelectionPolicy;
import org.reactfx.Subscription;
public class SpellCheckingDemo extends Application
{
private static final Set<String> dictionary = new HashSet<String>();
public static void main(String[] args)
{
launch(args);
}
@Override
public void start(Stage primaryStage)
{
StyleClassedTextArea textArea = new StyleClassedTextArea();
textArea.setWrapText(true);
Subscription cleanupWhenFinished = textArea.multiPlainChanges()
.successionEnds(Duration.ofMillis(500))
.subscribe(change ->
{
textArea.setStyleSpans(0, computeHighlighting(textArea.getText()));
});
// call when no longer need it: `cleanupWhenFinished.unsubscribe();`
textArea.setOnMouseClicked((MouseEvent mouseEvent) ->
{
if (mouseEvent.getButton().equals(MouseButton.SECONDARY))
{
if (mouseEvent.getClickCount() == 1)
{
CharacterHit hit = textArea.hit(mouseEvent.getX(), mouseEvent.getY());
int characterPosition = hit.getInsertionIndex();
// move the caret to that character's position
textArea.moveTo(characterPosition, SelectionPolicy.CLEAR);
}
}
});
textArea.setOnContextMenuRequested((ContextMenuEvent event) ->
{
if (event.getTarget() instanceof Text)
{
textArea.selectWord();
String referenceWord = textArea.getSelectedText();
textArea.deselect();
textArea.moveTo(textArea.getCaretPosition() - 1);
if (!dictionary.contains(StringUtils.trim(StringUtils.lowerCase(referenceWord))))
{
ContextMenu context = new ContextMenu();
for (String word : SpellCheckingDemo.getClosestWords(referenceWord))
{
MenuItem item = new MenuItem(word);
item.setOnAction((ActionEvent a) ->
{
textArea.selectWord();
textArea.replaceSelection(word);
textArea.deselect();
});
context.getItems().add(item);
}
context.show(primaryStage, event.getScreenX(), event.getScreenY());
}
}
});
// load the dictionary
try (InputStream input = SpellCheckingDemo.class.getResourceAsStream("/spellchecking.dict");
BufferedReader br = new BufferedReader(new InputStreamReader(input)))
{
String line;
while ((line = br.readLine()) != null)
{
dictionary.add(line);
}
} catch (IOException e)
{
e.printStackTrace();
}
// load the sample document
InputStream input2 = SpellCheckingDemo.class.getResourceAsStream("/spellchecking.txt");
try (java.util.Scanner s = new java.util.Scanner(input2))
{
String document = s.useDelimiter("\\A").hasNext() ? s.next() : "";
textArea.replaceText(0, 0, document);
}
Scene scene = new Scene(new StackPane(new VirtualizedScrollPane<>(textArea)), 600, 400);
scene.getStylesheets().add(SpellCheckingDemo.class.getResource("/spellchecking.css").toExternalForm());
primaryStage.setScene(scene);
primaryStage.setTitle("Spell Checking Demo");
primaryStage.show();
}
private static StyleSpans<Collection<String>> computeHighlighting(String text)
{
StyleSpansBuilder<Collection<String>> spansBuilder = new StyleSpansBuilder<>();
BreakIterator wb = BreakIterator.getWordInstance();
wb.setText(text);
int lastIndex = wb.first();
int lastKwEnd = 0;
while (lastIndex != BreakIterator.DONE)
{
int firstIndex = lastIndex;
lastIndex = wb.next();
if (lastIndex != BreakIterator.DONE
&& Character.isLetterOrDigit(text.charAt(firstIndex)))
{
String word = text.substring(firstIndex, lastIndex).toLowerCase();
if (!dictionary.contains(word))
{
spansBuilder.add(Collections.emptyList(), firstIndex - lastKwEnd);
spansBuilder.add(Collections.singleton("underlined"), lastIndex - firstIndex);
lastKwEnd = lastIndex;
}
//System.err.println();
}
}
spansBuilder.add(Collections.emptyList(), text.length() - lastKwEnd);
return spansBuilder.create();
}
public static List<String> getClosestWords(String word)
{
List<Pair<Double, String>> allWordDistances = new ArrayList<>();
JaroWinklerDistance distance = new JaroWinklerDistance();
for (String checkWord : dictionary)
{
allWordDistances.add(new Pair(distance.apply(StringUtils.lowerCase(word), checkWord), checkWord));
}
allWordDistances.sort(Comparator.comparingDouble(Pair::getKey));
List<String> closestWords = new ArrayList<>();
for (Pair pair : allWordDistances.subList(allWordDistances.size() - 5, allWordDistances.size()))
{
String addWord;
if (StringUtils.isAllUpperCase(word))
{
addWord = StringUtils.upperCase(String.valueOf(pair.getValue()));
} else if (CharUtils.isAsciiAlphaUpper(word.charAt(0)))
{
addWord = WordUtils.capitalize(String.valueOf(pair.getValue()));
} else
{
addWord = StringUtils.lowerCase(String.valueOf(pair.getValue()));
}
closestWords.add(addWord);
}
Collections.reverse(closestWords);
return closestWords;
}
private static class Pair<S, T>
{
public final S x;
public final T y;
public Pair(S x, T y)
{
this.x = x;
this.y = y;
}
public T getValue()
{
return y;
}
public S getKey()
{
return x;
}
@Override
public String toString()
{
return String.valueOf(x) + " : " + String.valueOf(y);
}
}
}
Download the full English dictionary here: https://github.com/dwyl/english-words/blob/master/words_alpha.txt
Answered By - trilogy
Answer Checked By - Pedro (JavaFixing Volunteer)