How to control Caret position in trimmed TextField? - javafx

I have a TextField, in which I do a check after any changes are made. I also trim away any double spaces that were typed. Problem is that after I replace text with trimmed one, Caret position refreshes. I added calculating, so that Caret will go to position user last input, but it's not working properly with extra spaces. My code:
EventHandler<InputEvent> fieldChangeListener = new EventHandler<InputEvent>() {
public void handle(InputEvent event) {
TextField field = (TextField) event.getSource();
int caretPos = field.getCaretPosition();
String text = field.getText();
text = text.replaceAll("\\s+", " ").trim();
field.setText(text);
field.positionCaret(caretPos);
event.consume();}};
name.addEventHandler(KeyEvent.KEY_RELEASED, fieldChangeListener);
In picture user puts cursor before word 'Friend', presses space bar and extra space will get trimmed, placing Caret right after 'F'. If space bar will be hold longer, Caret will be placed in the end.
Desired result: Caret placed before word 'Friend'.
How would be better to do that?

The reason why the caret position is not working as expected is because you are not reducing the caret position when there is multiple space in the string. You should remove the caret position by the amount of removed multiple spaces.
In the code below, I check if the string has at least double spaces and if it does then i remove the spaces and also reduce the caret position by the number of spaces removed from the string.
import javafx.application.Application;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.control.TextField;
import javafx.scene.input.InputEvent;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.AnchorPane;
import javafx.stage.Stage;
public class Miner extends Application {
#Override
public void start(Stage primaryStage) throws Exception {
AnchorPane pane = new AnchorPane();
TextField tf = new TextField("HELLO FRIEND");
int CaretPos = tf.getCaretPosition();
System.out.println("CaretPos = " + CaretPos);
pane.getChildren().add(tf);
EventHandler<InputEvent> fieldChangeListener = new EventHandler<InputEvent>() {
public String AtLeastDoubleSpace = " ";
public void handle(InputEvent event) {
TextField field = (TextField) event.getSource();
String text = field.getText();
String originalString = text;
int caretPos;
if(text.contains(AtLeastDoubleSpace)) {
caretPos = field.getCaretPosition();
text = text.replaceAll("\\s+"," ").trim();
field.setText(text);
int spacesRemoved = originalString.length() - text.length();
field.positionCaret(caretPos - spacesRemoved);
event.consume();
} else {
caretPos = field.getCaretPosition();
field.setText(text);
field.positionCaret(caretPos);
event.consume();
}
}};
tf.addEventHandler(KeyEvent.KEY_RELEASED, fieldChangeListener);
Scene scene = new Scene(pane);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
I provided this solution depending upon my understanding to the question you provided. Let me know, if you need any further help.

how about caretPositionProperty and add listener??
textField.caretPositionProperty().addListener((ob, old1, new1) -> {
System.out.println("old:" + old1 + " new " + new1);
});

Related

java.lang.IllegalArgumentException: Problem and Hitbox

I have two problems one this that, if i want to show score with the circle object:
layoutV.getChildren().addAll(virus, score);
I get the following error:
Exception in thread "JavaFX Application Thread" java.lang.IllegalArgumentException: Children: duplicate children added: parent = Pane#6661fc86[styleClass=root].
As far as I understand it is because the Task wants to show multiple scores. So should I use another scene or layout to show score?
My other problem is the hitbox of the object, right know everytime i click the score goes up. I looked up the mouse event getTarget but it does not seem like I can make it so that my object is the only target to use the mouse event on.
public class Main extends Application {
private Stage window;
private Pane layoutV;
private Scene scene;
private Circle virus;
private int score;
private Label scores;
#Override
public void start(Stage primaryStage) {
window = primaryStage;
window.setTitle("Enemy TEST");
this.score = 0;
scores = new Label("Score "+ score);
layoutV = new Pane();
scene = new Scene(layoutV, 600, 600);
window.setScene(scene);
window.show();
Thread th = new Thread(task);
th.setDaemon(true);
th.start();
}
Task task = new Task<Void>() {
#Override
protected Void call() throws Exception {
while (true) {
Platform.runLater(new Runnable() {
#Override
public void run() {
drawCircles();
}
});
Thread.sleep(1000);
}
}
};
public void drawCircles() {
double x = (double)(Math.random() * ((550 - 50) + 1)) + 50;
double y = (double)(Math.random() * ((550 - 50) + 1)) + 50;
double r = (double)(Math.random() * ((30 - 10) + 1)) + 10;
virus = new Circle(x, y, r, Color.VIOLET);
layoutV.setOnMouseClicked(e -> {
if (e.getButton() == MouseButton.PRIMARY) {
layoutV.getChildren().remove(e.getTarget());
this.score++;
System.out.println("score: "+ this.score);
}
});
layoutV.getChildren().addAll(virus);
scene.setRoot(layoutV);
window.setScene(scene);
}
public static void main(String[] args) {
launch(args);
}
}
You have lots of issues, not just the ones from your question:
Although it will work as you coded
it, I don't advise spawning a thread to draw your circles, instead
see:
JavaFX periodic background task
You don't need to set the root in the scene and the scene in the
window every time you draw a new circle.
Nor do you need to set the
mouse handler on the layout every time you draw a circle.
Rather than setting a mouse handler on the layout, you are better off setting a mouse handler on the circles themselves (which you can do before you add them to the scene).
score is an int, not a node you can only add nodes to the scene
graph.
See the documentation for the scene package:
A node may occur at most once anywhere in the scene graph. Specifically, a node must appear no more than once in the children list of a Parent or as the clip of a Node. See the Node class for more details on these restrictions.
How you are adding the node more than once is not clear to me, because you are probably doing it in code different than the Main class you provided.
To add a circle with a score on top, use a StackPane with the score in a label, but make the label mouse transparent, so that it does not register any clicks:
Label scoreLabel = new Label(score + "");
scoreLabel.setMouseTransparent(true);
StackPane balloon = new StackPane(circle, scoreLabel);
layoutV.getChildren.add(balloon);
Add the click handler on the balloon.
And additional issues I don't detail here but are solved in the demo code provided.
To fix all your errors, I would write some code like below. Perhaps you can review it and compare it with your code to help understand one way to create this game.
The example code might not be exactly the functionality you are looking for (that is not really its purpose), but it should be enough to keep you on the right track for implementing your application.
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.text.*;
import javafx.stage.Stage;
import javafx.util.Duration;
import java.util.concurrent.ThreadLocalRandom;
public class Inoculation extends Application {
public static final int W = 600;
public static final int H = 600;
private final IntegerProperty score = new SimpleIntegerProperty(0);
private final Pane playingField = new Pane();
#Override
public void start(Stage stage) {
StackPane overlay = createOverlay();
Pane layout = new StackPane(playingField, overlay);
stage.setScene(new Scene(layout, W, H));
stage.show();
Infection infection = new Infection(playingField, score);
infection.begin();
}
private StackPane createOverlay() {
Label totalScoreLabel = new Label();
totalScoreLabel.textProperty().bind(
Bindings.concat(
"Score ", score.asString()
)
);
StackPane overlay = new StackPane(totalScoreLabel);
StackPane.setAlignment(totalScoreLabel, Pos.TOP_LEFT);
overlay.setMouseTransparent(true);
return overlay;
}
public static void main(String[] args) {
launch(args);
}
}
class Infection {
private static final Duration SPAWN_PERIOD = Duration.seconds(1);
private static final int NUM_SPAWNS = 10;
private final Timeline virusGenerator;
public Infection(Pane playingField, IntegerProperty score) {
virusGenerator = new Timeline(
new KeyFrame(
SPAWN_PERIOD,
event -> spawnVirus(
playingField,
score
)
)
);
virusGenerator.setCycleCount(NUM_SPAWNS);
}
public void begin() {
virusGenerator.play();
}
private void spawnVirus(Pane playingField, IntegerProperty score) {
Virus virus = new Virus();
virus.setOnMouseClicked(
event -> {
score.set(score.get() + virus.getVirusScore());
playingField.getChildren().remove(virus);
}
);
playingField.getChildren().add(virus);
}
}
class Virus extends StackPane {
private static final int MAX_SCORE = 3;
private static final int RADIUS_INCREMENT = 10;
private final int virusScore = nextRandInt(MAX_SCORE) + 1;
public Virus() {
double r = (MAX_SCORE + 1 - virusScore) * RADIUS_INCREMENT;
Circle circle = new Circle(
r,
Color.VIOLET
);
Text virusScoreText = new Text("" + virusScore);
virusScoreText.setBoundsType(TextBoundsType.VISUAL);
virusScoreText.setMouseTransparent(true);
getChildren().setAll(
circle,
virusScoreText
);
setLayoutX(nextRandInt((int) (Inoculation.W - circle.getRadius() * 2)));
setLayoutY(nextRandInt((int) (Inoculation.H - circle.getRadius() * 2)));
setPickOnBounds(false);
}
public int getVirusScore() {
return virusScore;
}
// next random int between 0 (inclusive) and bound (exclusive)
private int nextRandInt(int bound) {
return ThreadLocalRandom.current().nextInt(bound);
}
}
Some additional notes on this implementation that might be useful to know:
The total score is placed in an overlayPane so that it is not obscured by elements added to the playingField (which contains the virus spawns).
The overlayPane is made mouseTransparent, so that it won't intercept any mouse events, and the clicks will fall through to the items in the playing field.
The app currently generates viruses within a fixed field size, regardless of whether you resize the window. That is just the way it is designed and coded, you could code it otherwise if wished. It would be more work to do so.
The Bindings class is used to create a string expression binding which concatenates the static string "Score " with an integer property representing the score. This allows the string representing the score to be bound to the score label text in the overlay so that it automatically updates whenever the score is changed.
The virus generation uses a timeline and is based on the concepts from:
JavaFX periodic background task
The application class is kept deliberately simple to handle mostly just the core application lifecycle, and the actual functionality of the application is abstracted to an Infection class which handles the spawning of the virus and a Virus class that generates a new virus.
This technique is used to center a score for each individual virus on the virus:
how to put a text into a circle object to display it from circle's center?
The virus itself is laid out in a StackPane. The pane has pick on bounds set to false. To remove the virus infection, you must click on the circle which represents the virus and not just anywhere in the square for the stack pane.
Because circle coordinates are in local co-ordinates and the circle is in a parent stack pane representing the virus, the circle itself does not need x and y values set, instead layout x and y values are set on the enclosing pane to allow positioning of the pane representing the entire virus.
The following technique is used to generate random integers in acceptable ranges using ThreadLocalRandom:
How do I generate random integers within a specific range in Java?

JavaFX: How to scroll to a specific line in the textarea

package sample;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.TextArea;
import javafx.stage.Stage;
import java.util.stream.IntStream;
public class Main extends Application {
#Override
public void start(Stage primaryStage) throws Exception{
final StringBuilder sb = new StringBuilder();
IntStream.range(1, 100).forEach(i -> sb.append("Line " + i + "\n"));
TextArea ta = new TextArea();
ta.setText(sb.toString());
//how to I get line 30 at top of the visible textarea
double someValue = 0;
ta.setScrollTop(someValue);
primaryStage.setTitle("Hello World");
primaryStage.setScene(new Scene(ta, 300, 300));
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
How do I get line 30 at top of the visible textarea?
I think 'someValue' should be relative to the total height which can be scrolled
But what is the total height that can be scrolled
This is the result I want to achieve:
This is a bit tricky. We could just determine each line height and call ta.setScrollTop((line - 1) * lineHeight);, but we do not know what line spacing TextArea uses.
But I found that TextAreaSkin contains public methods for determining bounds for any selected character, we just need to know its index.
#Override
public void start(Stage primaryStage) throws Exception{
final StringBuilder sb = new StringBuilder();
IntStream.range(1, 100).forEach(i -> sb.append("Line " + i + "\n"));
TextArea ta = new TextArea();
ta.setText(sb.toString());
// TextArea did not setup its skin yet, so we can't use it right now.
// We just append our task to the user tasks queue.
Platform.runLater(() -> {
// Define desired line
final int line = 30;
// Index of the first character in line that we look for.
int index = 0;
// for this example following line will work:
// int index = ta.getText().indexOf("Line " + line);
// for lines that do not contain its index we rely on "\n" count
int linesEncountered = 0;
boolean lineFound = false;
for (int i = 0; i < ta.getText().length(); i++) {
// count characters on our way to our desired line
index++;
if(ta.getText().charAt(i) == '\n') {
// next line char encountered
linesEncountered++;
if(linesEncountered == line-1) {
// next line is what we're looking for, stop now
lineFound = true;
break;
}
}
}
// scroll only if line found
if(lineFound) {
// Get bounds of the first character in the line using internal API (see comment below the code)
Rectangle2D lineBounds = ((com.sun.javafx.scene.control.skin.TextAreaSkin) ta.getSkin()).getCharacterBounds(index);
// Scroll to the top-Y of our line
ta.setScrollTop(lineBounds.getMinY());
}
});
primaryStage.setTitle("Hello World");
primaryStage.setScene(new Scene(ta, 300, 300));
primaryStage.show();
}
This solution works on Java 8, on 9+ TextAreaSkin was moved to the public package, so everything you need to make it work is to replace com.sun.javafx.scene.control.skin.TextAreaSkin with javafx.scene.control.skin.TextAreaSkin

How to control specific text selection in a TextArea (JavaFX)?

I'm using JavaFX and have a TextArea. In the textArea I need some way to ensure the user may only select one line at a time. So for my delete button what I have so far is:
deleteButton.setOnAction(e -> {
String s = "DELETE: ";
s += receipt.getSelectedText();
receipt.replaceSelection(s);
});
How can I enforce that user can only select one full line at a time? Each line will have \n as a breaker, so I was thinking I can some how use it as a key. An issue is the users ability to select more than one line at a time or a partial line. And yes, I must use textArea. I have right now where the delete button is reading what got deleted and displays it. The readt of my code is working great with this one issue. I have about 15 classes that all take in textAreas as a parameter where, when a button is clicked, it appends it to the TextArea and then it saves it to a specified object as a certain attribute. I just need highlight control, or a way to added a checkbox or a way to read where the user clicks that highlights the entire row (but if the click somewhere else, it highlights/selects that line, or try to highlight themselves, it doesn't let them).
You can use the filter in a TextFormatter to intercept and modify changes to the selection.
Here is a quick example. You could also modify the change to actually effect the functionality you are looking for on delete as well.
import java.util.function.UnaryOperator;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.TextArea;
import javafx.scene.control.TextFormatter;
import javafx.scene.control.TextFormatter.Change;
import javafx.stage.Stage;
public class TextAreaSelectByLine extends Application {
#Override
public void start(Stage primaryStage) {
TextArea textArea = new TextArea();
UnaryOperator<Change> filter = c -> {
int caret = c.getCaretPosition();
int anchor = c.getAnchor() ;
if (caret != anchor) {
int start = caret ;
int end = caret ;
String text = c.getControlNewText();
while (start > 0 && text.charAt(start) != '\n') start-- ;
while (end < text.length() && text.charAt(end) != '\n') end++ ;
c.setAnchor(start);
c.setCaretPosition(end);
}
return c ;
};
TextFormatter<String> formatter = new TextFormatter<>(filter);
textArea.setTextFormatter(formatter);
Scene scene = new Scene(textArea, 600, 600);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}

String with numbers and letters to double javafx

Hi I am trying to read a the numbers from a text field that shows a price e.g. £3.00, and convert the value of the price to a double. Is there a way to do
Double value;
value = Double.parseDouble(textField.getText());
But it won't let me do that because of the £ sign. Is there a way to strip the pound sign then read the digits.
Thanks
There is some TextFormatter and change filter handling logic built into the JavaFX TextField API, you could make use of that.
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import javafx.util.StringConverter;
import java.text.DecimalFormat;
import java.text.ParseException;
class CurrencyFormatter extends TextFormatter<Double> {
private static final double DEFAULT_VALUE = 5.00d;
private static final String CURRENCY_SYMBOL = "\u00A3"; // british pound
private static final DecimalFormat strictZeroDecimalFormat
= new DecimalFormat(CURRENCY_SYMBOL + "###,##0.00");
CurrencyFormatter() {
super(
// string converter converts between a string and a value property.
new StringConverter<Double>() {
#Override
public String toString(Double value) {
return strictZeroDecimalFormat.format(value);
}
#Override
public Double fromString(String string) {
try {
return strictZeroDecimalFormat.parse(string).doubleValue();
} catch (ParseException e) {
return Double.NaN;
}
}
},
DEFAULT_VALUE,
// change filter rejects text input if it cannot be parsed.
change -> {
try {
strictZeroDecimalFormat.parse(change.getControlNewText());
return change;
} catch (ParseException e) {
return null;
}
}
);
}
}
public class FormattedTextField extends Application {
public static void main(String[] args) { launch(args); }
#Override
public void start(final Stage stage) {
TextField textField = new TextField();
textField.setTextFormatter(new CurrencyFormatter());
Label text = new Label();
text.textProperty().bind(
Bindings.concat(
"Text: ",
textField.textProperty()
)
);
Label value = new Label();
value.textProperty().bind(
Bindings.concat(
"Value: ",
textField.getTextFormatter().valueProperty().asString()
)
);
VBox layout = new VBox(
10,
textField,
text,
value,
new Button("Apply")
);
layout.setPadding(new Insets(10));
stage.setScene(new Scene(layout));
stage.show();
}
}
The exact rules for DecimalFormat and the filter could get a little tricky if you are very particular about user experience (e.g. can the user enter the currency symbol? what happens if the user does not enter a currency symbol? are empty values permitted? etc.) The above example offers a compromise between a reasonable user experience and a (relatively) easy to program solution. For an actual production level application, you might wish to tweak the logic and behavior a bit more to fit your particular application.
Note, the apply button doesn't actually need to do anything to apply the change. Changes are applied when the user changes focus away from the text field (as long as they pass the change filter). So if the user clicks on the apply button, it gains, focus, the text field loses focus and the change is applied if applicable.
The above example treats the currency values as doubles (to match with the question), but those serious about currency may wish to look to BigDecimal.
For a simpler solution using similar concepts, see also:
Java 8 U40 TextFormatter (JavaFX) to restrict user input only for decimal number

JavaFX ChoiceBox add separator with type safety

I'm looking to add a separator into a choice box and still retain the type safety.
On all of the examples I've seen, they just do the following:
ChoiceBox<Object> cb = new ChoiceBox<>();
cb.getItems().addAll("one", "two", new Separator(), "fadfadfasd", "afdafdsfas");
Has anyone come up with a solution to be able to add separators and still retain type safety?
I would expect that if I wanted to add separators, I should be able do something along the following:
ChoiceBox<T> cb = new ChoiceBox<T>();
cb.getSeparators().add(1, new Separator()); // 1 is the index of where the separator should be
I shouldn't have to sacrifice type safety just to add separators.
As already noted, are Separators only supported if added to the items (dirty, dirty). To support them along the lines expected in the question, we need to:
add the notion of list of separator to choiceBox
make its skin aware of that list
While the former is not a big deal, the latter requires a complete re-write (mostly c&p) of its skin, as everything is tightly hidden in privacy. If the re-write has happened anyway, then it's just a couple of lines more :-)
Just for fun, I'm experimenting with ChoiceBoxX that solves some nasty bugs in its selection handling, so couldn't resist to try.
First, add support to the ChoiceBoxx itself:
/**
* Adds a separator index to the list. The separator is inserted
* after the item with the same index. Client code
* must keep this list in sync with the data.
*
* #param separator
*/
public final void addSeparator(int separator) {
if (separatorsList.getValue() == null) {
separatorsList.setValue(FXCollections.observableArrayList());
}
separatorsList.getValue().add(separator);
};
Then some changes in ChoiceBoxXSkin
must listen to the separatorsList
must expect index-of-menuItem != index-of-choiceItem
menuItem must keep its index-of-choiceItem
At its simplest, the listener re-builds the popup, the menuItem stores the dataIndex in its properties and all code that needs to access a popup by its dataIndex is delegated to a method that loops through the menuItems until it finds one that fits:
protected RadioMenuItem getMenuItemFor(int dataIndex) {
if (dataIndex < 0) return null;
int loopIndex = dataIndex;
while (loopIndex < popup.getItems().size()) {
MenuItem item = popup.getItems().get(loopIndex);
ObservableMap<Object, Object> properties = item.getProperties();
Object object = properties.get("data-index");
if ((object instanceof Integer) && dataIndex == (Integer) object) {
return item instanceof RadioMenuItem ? (RadioMenuItem)item : null;
}
loopIndex++;
}
return null;
}
Well you can work around it by creating an interface and then subclassing Separator to implement this interface:
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.ChoiceBox;
import javafx.scene.control.Separator;
import javafx.scene.layout.GridPane;
import javafx.scene.text.Text;
import javafx.stage.Stage;
public class ChoiceBoxIsSafe extends Application {
interface FruitInterface { }
static public class Fruit implements FruitInterface {
private StringProperty name = new SimpleStringProperty();
Fruit(String name) {
this.name.set(name);
}
public StringProperty nameProperty() {
return name;
}
#Override
public String toString() {
return name.get();
}
}
static public class FruitySeparator extends Separator implements FruitInterface { }
#Override
public void start(Stage primaryStage) throws Exception {
GridPane grid = new GridPane();
grid.setHgap(10); grid.setVgap(10); grid.setPadding(new Insets(10));
ChoiceBox<FruitInterface> cb = new ChoiceBox<>();
cb.getItems().addAll(new Fruit("Apple"), new Fruit("Orange"), new FruitySeparator(), new Fruit("Peach"));
Text text = new Text("");
ReadOnlyObjectProperty<FruitInterface> selected = cb.getSelectionModel().selectedItemProperty();
text.textProperty().bind(Bindings.select(selected, "name"));
grid.add(cb, 0, 0);
grid.add(text, 1, 0);
Scene scene = new Scene(grid);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
but that is hardly an "elegant" solution and cannot be done in all cases (e.g. ChoiceBox<String>).
From the implementation of ChoiceBox it certainly looks like it wasn't a good idea to treat Separators like items in the ChoiceBox :-(.
FOR THE REST OF US:
There is a MUCH easier way to do this using code (there are easy ways to do it using FXML too, doing it in code offers more flexibility).
You simply create an ObservableList, then populate it with your items, including the separator then assign that list to the ChoiceBox like this:
private void fillChoiceBox(ChoiceBox choiceBox) {
ObservableList items = FXCollections.observableArrayList();
items.add("one");
items.add("two");
items.add("three");
items.add(new Separator());
items.add("Apples");
items.add("Oranges");
items.add("Pears");
choiceBox.getItems().clear();
choiceBox.getItems().addAll(items);
}

Resources