Reputation:
I have a javafx program and I am trying to locate a certain phrase in a text area. The phrase is entered by the user via a TextInputDialog control. I then handle the Next button inside the dialog that should locate and style the found text in the text area when its clicked. I pass an instance of the return type of the method TextInputDialog.showAndWait()
to the action methods for the next button. I try to check if the result is present in the type Optional<String>
and it doesn't return true or false, also checking if it's null via the method result.isEmpty()
does not return true or false. How do I change my code to make it achieve the desired objective
//method to locate text inside the text area
@FXML
protected void onLocateText(){
//show an alert for the user to enter text
TextInputDialog dialog = new TextInputDialog();
dialog.setTitle("Find");
dialog.setHeaderText("Enter the search phrase");
//set buttons for iterating through the next and previous words
ButtonType next = new ButtonType("Next", ButtonBar.ButtonData.OK_DONE);
ButtonType previous = new ButtonType("Previous",ButtonBar.ButtonData.CANCEL_CLOSE);
//what happens when the next button is clicked;
//add the buttons to the prompt
dialog.getDialogPane().getButtonTypes().setAll(next, previous);
Button nxt = (Button) dialog.getDialogPane().lookupButton(next);
Button prev = (Button) dialog.getDialogPane().lookupButton(previous);
Optional<String> result = dialog.showAndWait();
nxt.setOnAction(e-> handleNextClick(result));
prev.setOnAction(e-> handlePreviousClick(result));
}
The implementation details of handleNextClick
private void handleNextClick(Optional<String> result) {
//show and wait for the user input
if (result.isPresent()) {
//get the text entered
String text = result.get();
System.out.println(text);
//loop through the text in the text area and locate
Pattern pattern = Pattern.compile(text);
Matcher matcher = pattern.matcher(text);
while (matcher.find()) {
Text textNode = new Text(matcher.group());
textNode.setStyle("-fx-fill: red; -fx-font-weight: bold;");
//replace the matched text with the formatted text
textArea.replaceText(matcher.start(), matcher.end(), "");
}
}else{
System.out.println("block returning false");
}
}
Upvotes: 1
Views: 199
Reputation: 159416
A basic search implementation leveraging the text selection capability of TextArea.
This works (somewhat) similarly to the Windows Notepad app and the Mac TextEdit app, but with a bit less bling.
The example screen shows the output after pressing the next button twice.
If a more sophisticated solution is required, look at Sai's answer.
There are a lot of subtle corner cases, like whether to disable or disable next and previous buttons if no next match is available in a given direction and if not, how to handle such a situation (in this solution's case it just removes the selection). So the solution may need adjustments and may not be directly applicable to your case. Also, the solution has been minimally tested, so may have some errors (if using it in your application test it thoroughly first).
The solution is based on a non-editable text area. It might also work with an editable text area, but I didn't try it.
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.IndexRange;
import javafx.scene.control.TextArea;
import javafx.scene.control.TextField;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class FindItApp extends Application {
private final TextField searchBox = new TextField(DEFAULT_SEARCH_TEXT);
private final Button previous = new Button("<");
private final Button next = new Button(">");
private final TextArea textArea = new TextArea(TEXT);
@Override
public void start(Stage stage) {
previous.setOnAction(e -> findPrevious());
next.setOnAction(e -> findNext());
searchBox.setOnAction(e -> next.fire());
HBox controls = new HBox(10, searchBox, previous, next);
textArea.setEditable(false);
VBox layout = new VBox(10, controls, textArea);
layout.setPadding(new Insets(10));
stage.setScene(new Scene(layout, 250, 200));
stage.show();
}
private void findPrevious() {
String text = textArea.getText();
String searchText = searchBox.getText();
if (text == null || text.isEmpty() || searchText == null || searchText.isEmpty()) {
textArea.selectRange(0,0);
return;
}
IndexRange selection = textArea.getSelection();
int endPos = selection == null || selection.getStart() == 0 ? text.length() : selection.getStart();
int textPos = text.substring(0, endPos).lastIndexOf(searchText);
if (textPos == -1) {
textArea.selectRange(0,0);
return;
}
textArea.selectRange(textPos, textPos + searchText.length());
}
private void findNext() {
String text = textArea.getText();
String searchText = searchBox.getText();
if (text == null || text.isEmpty() || searchText == null || searchText.isEmpty()) {
textArea.selectRange(0,0);
return;
}
IndexRange selection = textArea.getSelection();
int startPos = selection != null ? selection.getEnd() : 0;
int textPos = text.indexOf(searchText, startPos);
if (textPos == -1) {
textArea.selectRange(0,0);
return;
}
textArea.selectRange(textPos, textPos + searchText.length());
}
public static void main(String[] args) {
launch(args);
}
private static final String TEXT = """
If we shadows have offended,
Think but this, and all is mended,
That you have but slumber’d here,
While these visions did appear,
And this weak and idle theme,
No more yielding, but a dream.
""";
private static final String DEFAULT_SEARCH_TEXT = "but";
}
Upvotes: 0
Reputation: 9959
+1 for all the suggestions provided in the comments. I do recommend to use TextFlow for getting a better styled text.
I also like @jewelsea suggestion of selecting the required text (to keep it simple) . But keep a note that, this approach has few limitations. You can only change the color but not the text style. And the selection/highlighting will be cleared, the moment you try to select the text manually.
But if you say you need to get this behavior with TextArea and you need to still have the selection behavior (while your text is highlighted), you can check the below solution.
Firstly, I recommend to work with a Stage rather than using Dialog. This way you have better control over the buttons.
Secondly, I created a custom TextArea control, that has an extra feature of highlighting the text. This logic is same as how TextArea selection works (like using Path for building selection area). And for this Path, I am using BlendMode.ADD to let the color apply on text instead of background.
The complete working demo is below. This is just to give you some ideas. If you think this looks like an overkill for your requirement, then ignore this approach and stick to @jewelsea suggestion.
Note: This may not cover all scenarios. Check the logic thoroughly and fix if you are going with this approach ;)
import javafx.application.Application;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.geometry.Insets;
import javafx.geometry.Point2D;
import javafx.geometry.Pos;
import javafx.geometry.Rectangle2D;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextArea;
import javafx.scene.control.TextField;
import javafx.scene.control.skin.TextAreaSkin;
import javafx.scene.effect.BlendMode;
import javafx.scene.layout.*;
import javafx.scene.shape.LineTo;
import javafx.scene.shape.MoveTo;
import javafx.scene.shape.Path;
import javafx.scene.shape.PathElement;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class TextAreaSearchDemo extends Application {
String text = "One day he was sitting on a rock, watching the goats, and he heard something in the bushes." +
"A lion was moving slowly toward one of the goats. Wilson knew that he had to be brave." +
"He thought about what he had learned from his father. He sat quietly, waiting for the right" +
"moment. He knew that if he did the wrong thing, the lion would kill him. Wilson lifted his" +
"spear and moved quietly toward the lion. When the lion saw him he looked him in the eye." +
"They both froze. Wilson remembered all that his father had told him about being brave." +
"He knew that he could throw his spear and kill the lion. Suddenly the lion turned away and" +
"disappeared over the rocks. Wilson stood strong and watched. After the danger passed he" +
"took a deep breath. He felt the sun on his face and put down his spear. Wilson knew that he" +
"could take care of the people in his family and his tribe.";
@Override
public void start(final Stage stage) throws Exception {
Button search = new Button("Search");
CustomTextArea textArea = new CustomTextArea();
textArea.setText(text);
textArea.setWrapText(true);
VBox.setVgrow(textArea, Priority.ALWAYS);
search.setOnAction(e -> {
IntegerProperty index = new SimpleIntegerProperty(-1);
TextField textField = new TextField();
textField.textProperty().addListener(p -> index.set(-1));
HBox pane = new HBox(10, new Label("Enter the search phrase"), textField);
Button nextBtn = new Button("Next");
Button prevBtn = new Button("Previous");
nextBtn.setOnAction(e1 -> handleSearch(textField.getText(), true, textArea, index));
prevBtn.setOnAction(e1 -> handleSearch(textField.getText(), false, textArea, index));
HBox btnPane = new HBox(10, nextBtn, prevBtn);
btnPane.setAlignment(Pos.CENTER);
VBox root = new VBox(20, pane, btnPane);
root.setPadding(new Insets(20));
Stage dialog = new Stage();
dialog.initStyle(StageStyle.UTILITY);
dialog.setTitle("Find");
dialog.setScene(new Scene(root, 350, 150));
dialog.show();
});
VBox root = new VBox(15, search, textArea);
root.setPadding(new Insets(10));
Scene scene = new Scene(root, 300, 400);
stage.setScene(scene);
stage.setTitle("TextArea Search");
stage.show();
}
private void handleSearch(final String text, final boolean isNext, CustomTextArea textArea, IntegerProperty index) {
if (text == null || text.isEmpty()) {
return;
}
int newIndex = isNext ? index.get() + 1 : index.get() - 1;
Pattern pattern = Pattern.compile(text);
Matcher matcher = pattern.matcher(textArea.getText());
int count = 0;
while (matcher.find()) {
if (count == newIndex) {
textArea.highlightRange(matcher.start(), matcher.end() - 1);
index.set(newIndex);
break;
}
count++;
}
}
class CustomTextArea extends TextArea {
/**
* Start index of the highlight.
*/
private int highlightStartIndex;
/**
* End index of the highlight.
*/
private int highlightEndIndex;
/**
* Rectangle node to act as highlight.
*/
private final Path highlightPath;
/**
* Node to keep reference of the all contents of the TextArea.
*/
private StackPane contentPane;
boolean highlightRequired = false;
public CustomTextArea() {
/* Setting default values */
highlightStartIndex = -1;
highlightEndIndex = -1;
/* Settings for text highlighting */
highlightPath = new Path();
highlightPath.getStyleClass().add("textarea-highlight");
highlightPath.setMouseTransparent(true);
highlightPath.setManaged(false);
highlightPath.setStroke(null);
highlightPath.setStyle("-fx-fill:red;-fx-stroke-width:0px;");
highlightPath.setBlendMode(BlendMode.ADD);
textProperty().addListener((obs, oldVal, newVal) -> removeHighlight());
/*
* When the width of the TextArea changes, we need to update the selection path bounds.
*/
widthProperty().addListener((obs, oldVal, newVal) -> {
if (highlightStartIndex > -1) {
doHighlightIndex();
}
});
needsLayoutProperty().addListener((obs, oldval, needsLayout) -> {
if (!needsLayout && highlightRequired) {
doHighlightIndex();
highlightRequired = false;
}
});
}
/**
* Removes the highlight in the text area.
*/
public final void removeHighlight() {
if (contentPane != null) {
contentPane.getChildren().remove(highlightPath);
}
highlightStartIndex = -1;
highlightEndIndex = -1;
}
/**
* Highlights the character at the provided index in the text area.
*
* @param highlightPos Position of the character in the text
*/
public final void highlight(final int highlightPos) {
highlightRange(highlightPos, highlightPos);
}
/**
* Highlights the characters for the provided index range in the text area.
*
* @param start Start character index for highlighting
* @param end End character index for highlighting
*/
public final void highlightRange(final int start, final int end) {
if (end < start) {
throw new IllegalArgumentException("Caret cannot be less than the anchor index");
}
if (getLength() > 0) {
highlightStartIndex = start;
highlightEndIndex = end;
if (highlightStartIndex >= getLength()) {
highlightStartIndex = getLength() - 1;
} else if (highlightStartIndex < 0) {
highlightStartIndex = 0;
}
if (highlightEndIndex >= getLength()) {
highlightEndIndex = getLength() - 1;
} else if (highlightEndIndex < 0) {
highlightEndIndex = 0;
}
if (getSkin() != null) {
doHighlightIndex();
} else {
highlightRequired = true;
}
}
}
/**
* Highlights the character at the specified index.
*/
private void doHighlightIndex() {
if (highlightStartIndex > -1) {
/* Compute the highlight bounds based on the index range. Handles multi line highlighting as well. */
List<HighlightBound> highlightBounds = computeHighlightBounds();
/* Building the selection path based on the character bounds */
final List<PathElement> elements = new ArrayList<>();
highlightBounds.forEach(bound -> {
elements.add(new MoveTo(bound.point1.getX(), bound.point1.getY()));
elements.add(new LineTo(bound.point2.getX(), bound.point2.getY()));
elements.add(new LineTo(bound.point3.getX(), bound.point3.getY()));
elements.add(new LineTo(bound.point4.getX(), bound.point4.getY()));
elements.add(new LineTo(bound.point1.getX(), bound.point1.getY()));
});
highlightPath.getElements().clear();
highlightPath.getElements().addAll(elements);
/* Ensuring to lookup contentPane if it not yet loaded */
if (contentPane == null) {
lookupContentPane();
}
/* If the highlightPath is not yet added in the pane then adding in the contentPane */
if (contentPane != null && !contentPane.getChildren().contains(highlightPath)) {
contentPane.getChildren().add(highlightPath);
}
}
}
/**
* Lookup for the content pane in which all nodes are rendered.
*/
private void lookupContentPane() {
final Region content = (Region) lookup(".content");
if (content != null) {
contentPane = (StackPane) content.getParent();
}
}
@Override
protected final void layoutChildren() {
super.layoutChildren();
/*
* Looking for appropriate nodes that are required to determine text selection. Using these nodes, we build
* an extra node(Path) inside the TextArea to show the custom selection.
*/
if (contentPane == null) {
lookupContentPane();
}
}
public Path getHighlightPath() {
return highlightPath;
}
/**
* Computes the bounds for each highlight line based on the provided highlight indexes.
*
* @return List of HighLightBounds for multiple lines
*/
private List<HighlightBound> computeHighlightBounds() {
final List<HighlightBound> list = new ArrayList<>();
/* If it is single character highlighting, including only one character bounds. */
if (highlightEndIndex <= highlightStartIndex) {
/* If it is single character highlighting, including only one character bounds. */
final Rectangle2D bounds = ((TextAreaSkin) getSkin()).getCharacterBounds(highlightStartIndex);
list.add(new HighlightBound(bounds.getMinX(), bounds.getMinY(), bounds.getMaxX(), bounds.getMaxY()));
return list;
}
/* If it is a range highlighting... */
double minX = -1;
double minY = -1;
double maxX = -1;
double maxY = -1;
/*
* Looping through each character in the range and taking its bound to compute each line highlight bound
*/
for (int index = highlightStartIndex; index <= highlightEndIndex; index++) {
final Rectangle2D bounds = ((TextAreaSkin) getSkin()).getCharacterBounds(index);
if (index == highlightStartIndex) {
minX = bounds.getMinX();
minY = bounds.getMinY();
maxX = bounds.getMaxX();
maxY = bounds.getMaxY();
} else {
/* If the new character minX is less than previous minX, then it is a new line */
if (bounds.getMinX() <= minX) {
/* Registering the previous bounds for the line */
list.add(new HighlightBound(minX, minY, maxX, maxY));
/* ... and starting a new line bounds */
minX = bounds.getMinX();
minY = bounds.getMinY();
maxX = bounds.getMaxX();
maxY = bounds.getMaxY();
} else {
/*
* If the character falls next to the previous character, then updating the highlight end
* bounds
*/
maxX = bounds.getMaxX();
maxY = bounds.getMaxY();
}
}
}
/* Registering the last highlight bound */
if (minX > -1) {
list.add(new HighlightBound(minX, minY, maxX, maxY));
}
return list;
}
/**
* Class to hold the bounds of the highlight for each line. This class provided the four corners of the bounds.
*/
final class HighlightBound {
/* Top left point of the bound */
private final Point2D point1;
/* Top right point of the bound */
private final Point2D point2;
/* Bottom right point of the bound */
private final Point2D point3;
/* Bottom left point of the bound */
private final Point2D point4;
/**
* Constructor
*
* @param minX Minimun X value of the bound
* @param minY Minimum Y value of the bound
* @param maxX Maximum X value of the bound
* @param maxY Maximum Y value of the bound
*/
public HighlightBound(double minX, double minY, double maxX, double maxY) {
point1 = new Point2D(minX, minY);
point2 = new Point2D(maxX, minY);
point3 = new Point2D(maxX, maxY);
point4 = new Point2D(minX, maxY);
}
}
}
}
Upvotes: 2