JavaFx TextArea Fixed Content Row Count - java

I have a fixed TextArea with 50 lines. If the content reaches 50th line, the 1st line should remove and the 51st line should add to maintain the fixed row count.
This behavior is same as application console which hide previous inputs after some point.
Currently I am clearing the TextArea after reaches 50th lines using counter.
public static void updateTextAreaTest(String text) {
lineCount++;
Platform.runLater(() -> {
if (lineCount <= 50) {
txtAreaTest.appendText(text + "\n");
} else {
txtAreaTest.setText("");
lineCount = 0;
}
});
}
I need to hide the first line while adding new line without affecting the performance of the application which has so many threads running.
Edit:
The TextArea is not editable. TextArea will update automatically without user interaction.

As suggested in the comments, a ListView might be a preferred option for this.
However, if you want to use a text area, you can achieve this with by setting a TextFormatter on the text area. The filter for the TextFormatter can check the number of lines in the proposed new text for the text area, and if that contains more than the allowed number of lines, modify the change to drop the first lines. Note that this solution allows for inserting text with multiple lines in a single action.
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javafx.animation.Animation;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.TextArea;
import javafx.scene.control.TextFormatter;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
import javafx.util.Duration;
/**
* JavaFX App
*/
public class App extends Application {
private int lineNumber ;
private final int MAX_LINES = 50 ;
private TextArea createConsole() {
TextArea appConsole = new TextArea();
appConsole.setWrapText(false);
appConsole.setEditable(false);
Pattern newline = Pattern.compile("\n");
appConsole.setTextFormatter(new TextFormatter<String>(change -> {
String newText = change.getControlNewText();
// count lines in proposed new text:
Matcher matcher = newline.matcher(newText);
int lines = 1 ;
while (matcher.find()) lines++;
// if there aren't too many lines just return the changed unmodified:
if (lines <= MAX_LINES) return change ;
// drop first (lines - 50) lines and replace all text
// (there's no other way AFAIK to drop text at the beginning
// and replace it at the end):
int linesToDrop = lines - MAX_LINES ;
int index = 0 ;
for (int i = 0 ; i < linesToDrop ; i++) {
index = newText.indexOf('\n', index) ;
}
change.setRange(0, change.getControlText().length());
change.setText(newText.substring(index+1));
return change ;
}));
return appConsole;
}
#Override
public void start(Stage stage) {
TextArea appConsole = createConsole();
// Fill with 45 lines to start:
appConsole.appendText("Line 1");
for (lineNumber = 2 ; lineNumber <= 45 ; lineNumber++) {
appConsole.appendText("\nLine "+lineNumber);
}
// add a new line every 2 seconds:
Timeline demo = new Timeline(
new KeyFrame(Duration.seconds(2),
e -> appConsole.appendText("\nLine "+(lineNumber++))
)
);
demo.setCycleCount(Animation.INDEFINITE);
demo.play();
stage.setScene(new Scene(new BorderPane(appConsole)));
stage.show();
}
public static void main(String[] args) {
launch();
}
}
One nice thing about this solution is that it is a "fire and forget" configuration of the text area. You create the text area, set the formatter on it, which automatically provides the functionality of never having more than 50 lines of text, with lines at the beginning being dropped if needed, and then the rest of your code can just call TextArea methods (such as appendText()) as needed.

Related

JavaFX adding nodes to anchorpane in for loop, but waiting a few seconds each iteration before adding the next node [duplicate]

This question already has answers here:
JavaFX periodic background task
(6 answers)
Closed 1 year ago.
I am making a game, what should happen is, inside a for loop while iterating over an arraylist. Inside each loop I want to add a node to my scene, then I want to wait some time and add another node to the scene. the time between each iteration is also defined in the item of the arraylist, and can be different each iteration.
What i've tried:
//Circle is my own class, other than these attributes along with getters it has nothing in it.
//The last number is the time to wait.
//Also for testing i used a static arraylist, normally the items come from a json file.
ArrayList<Circle> lvlNodes = new ArrayList<>();
lvlNodes.add(new Circle(50,50, Color.RED,250,100));
lvlNodes.add(new Circle(200,100, Color.BLUE,500,250));
lvlNodes.add(new Circle(900,500, Color.YELLOW,750,500));
lvlNodes.add(new Circle(400,50, Color.GREEN,1000,500));
//Iterating over the arraylist and adding the nodes.
for (int i=0; i<lvlNodes.size(); i++) {
Circle currentNode = lvlNodes.get(i);
//Add the node the the anchor pane.
view.addLvlNode(currentNode, diameter); //diameter is a final value, all nodes in the game are the same and thus only added once at the top of the json file
//Wait and move on to the next one.
try {
Thread.sleep(currentNode.getTimeToNextNode())
} catch (InterruptedException e) {
System.out.println(e);
}
}
//Adds the next node to the game.
public void addLvlNode(Circle circle, double diameter) {
//Create node.
javafx.scene.shape.Circle lvlNode = new javafx.scene.shape.Circle(circle.getPosX(),
circle.getPosY(), diameter/2);
//Anchor node the the anchorpane.
AnchorPane.setTopAnchor(lvlNode, (double)circle.getPosY());
AnchorPane.setLeftAnchor(lvlNode, (double)circle.getPosX());
anchrLevel.getChildren().add(lvlNode);
lvlNode.toBack();
}//addLvlNode.
The Thread.sleep() works, each iteration in the for loop is with the time in between. but the nodes don't get added until the for loop is done iterating.
Is there any way of adding the nodes inside the for loop with the time amount in between?
The problem that you're having is that all of your code is running on the FXAT, and while the FXAT is busy running that code, it can't do things like update the screen to actually add the circles to it. So when your code is finished, including all of the Thread.sleep() calls, it then does all of the screen painting - all at once.
Rather than build your custom Circle class, I've just built some arrays to hold the Circles and wait times. Then I put the addLvlNode & sleep into a background job. The background job uses Platform.runLater() to add the nodes onto the Pane on the FXAT:
import javafx.application.Application;
import javafx.application.Platform;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.stage.Stage;
import java.util.ArrayList;
public class CircleMaker extends Application {
private Pane mainPain;
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) {
ArrayList<Circle> circles = new ArrayList<>();
ArrayList<Integer> waitTimes = new ArrayList<>();
circles.add(new Circle(50, 50, 125, Color.RED));
waitTimes.add(1000);
circles.add(new Circle(200, 200, 250, Color.BLUE));
waitTimes.add(2500);
circles.add(new Circle(900, 500, 375, Color.YELLOW));
waitTimes.add(5000);
circles.add(new Circle(400, 50, 500, Color.GREEN));
waitTimes.add(5000);
Thread waitingThread = new Thread(() -> {
for (int idx = 0; idx < 4; idx++) {
Circle thisCircle = circles.get(idx);
Platform.runLater(() -> addLvlNode(thisCircle));
try {
Thread.sleep(waitTimes.get(idx));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
mainPain = new Pane();
primaryStage.setScene(new Scene(mainPain, 1000, 800));
primaryStage.show();
waitingThread.start();
}
private void addLvlNode(Node circle) {
mainPain.getChildren().add(circle);
circle.toBack();
}
}
I also stretched out the wait times by a factor of 10 so that it was easier to see that the circles are each painted in turn with a pause between them.
You could probably use Timeline to do this, but since you said that the Circle data was eventually going to be JSON from an external source, that implies to me that you've probably got some kind of non-JavaFX mechanics happening so it's probably best to just use a background thread to do whatever you need. You can see from this example what the basic framework would be.

Multi-colored JLabel

I would like a different way to create a multi-colored JLabel.
(Multi-colored = parts of the text in different foreground-colors)
The only solution I found so far (and which I currently use), is setting the text in html. But I'm having problems with that...
When the LayoutManager decides that the JLabel should be narrowed, with a plain-text in a JLabel, the text gets kind of cropped, and "..." is added.
(e.g.: "My Long Text" -> becomes: "My Long T...")
With html inside a JLabel, the text is wrapped somewhere on a space-character, leaving the rest of outside the drawable area, and invisible as the JLabel's height is unchanged.
(e.g.: "My Long Text" -> becomes: "My Long")
In my case the JLabel is rendered in a JTable, which gets resized by the user, not to mention in different screen resolutions.
I tried adding a "nowrap" attribute or a ""-tag to the html, but it looks like this is ignored.
Leaving me -I think- with one solution: painting the label myself.
Or not?
Any suggestions?
Examples?
Thank you.
Here's a very simple example:
Try to resize this panel horizontally, and see what happens with the text inside both JLabel's...
(there's no indication for the user, that the text of the second JLabel is not the complete content)
-> In the example, the JLabel's height changes, but when rendered inside the framework's JTable, the height of the rows doesn't change and I don't want it to change. Without the use of HTML it doesn't change the height either...
import java.awt.BorderLayout;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
public class MultiJLabel
extends JFrame
{
public MultiJLabel()
{
super("Multi-colored JLabel test");
JPanel pnl = new JPanel();
pnl.setLayout(new BorderLayout());
pnl.add(new JLabel("This is a test of My Long Text"), BorderLayout.NORTH);
pnl.add(new JLabel("<html>This is a test of <font color='#ffbebe'>My Long Text</font></html>"), BorderLayout.SOUTH);
this.getContentPane().add(pnl);
this.pack();
this.setDefaultCloseOperation(EXIT_ON_CLOSE);
this.setVisible(true);
}
public static void main(String[] args)
{
new MultiJLabel();
}
}
Here's a picture of the original problem, where our users are not aware that the client's Order Number is not what the grid is showing, because this column has HTML-formatted text to show multi-colors.
Thank you all for your comments, but I was impatient and created my own JLabel.
I know it may be a poor programmed version, but it works for me...
You can test it by altering the above example with:
JMultiColorLabel lbl = new JMultiColorLabel("This is a test of My Long Text");
lbl.setColors(new int[]{10,15}, new Color[]{Color.RED,Color.BLUE});
lbl.setPreferredSize(new Dimension(200,20));
pnl.add(lbl, BorderLayout.SOUTH);
And use this class:
import java.awt.Color;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Insets;
import java.awt.RenderingHints;
import java.util.HashMap;
import javax.swing.JLabel;
public class JMultiColorLabel
extends JLabel
{
private static final String STRING_OVERFLOW = "...";
private HashMap<Integer, Color> extraColors = new HashMap<Integer, Color>();
public JMultiColorLabel(String text)
{
super(text);
}
public void setColors(int[] indices, Color[] colors)
{
for (int i = 0; i < indices.length; i++)
this.extraColors.put(indices[i], colors[i]);
}
protected void paintComponent(Graphics g)
{
// Get text-contents of Label
String text = this.getText();
// No text in the JLabel? -> No risk: super
if (text == null || text.length() == 0)
{
super.paintComponent(g);
return;
}
// Content Array of characters to paint
char[] chars = text.toCharArray();
// Draw nice and smooth
Graphics2D g2d = (Graphics2D)g;
g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
// Draw background
if (this.isOpaque())
{
g2d.setColor(this.getBackground());
g2d.fillRect(0, 0, this.getWidth(), this.getHeight());
}
// FontMetrics to calculate widths and height
FontMetrics fm = g2d.getFontMetrics();
// Available space
Insets ins = this.getInsets();
int maxSpace = this.getWidth()-(ins.left+ins.right);
boolean overflow = (fm.stringWidth(text) > maxSpace);
// Starting offset
int offset = ins.left+1;
// The start Color is the default
g2d.setColor(this.getForeground());
// Loop over characters
for (int i = 0; i < chars.length; i++)
{
// Switch Color?
if (this.extraColors.containsKey(i))
g2d.setColor(this.extraColors.get(i));
// Check if we still have enough room for this character
if (overflow && offset >= (maxSpace-fm.stringWidth(STRING_OVERFLOW)))
{ // Draw overflow and stop painting
g2d.drawString(STRING_OVERFLOW, offset, (fm.getHeight()+ins.top));
return;
}
else // We have the space -> Draw the character
g2d.drawString(String.valueOf(chars[i]), offset, (fm.getHeight()+ins.top));
// Move cursor to the next horizontal position
offset += fm.charWidth(chars[i]);
}
}
}
To prevent line wrapping when using html-text in JLabels, wrap the text in nobr (no-break) tags:
new JLabel("<html><nobr>This is a test of <font color='#ffbebe'>My Long Text</font></nobr></html>")
When using the nobr-tags, the line will not be wrapped, but it won't be truncated as well. So, there won't be any ellipsis (...) at the end of the shown text, but it will just cut off.
The missing ... might actually be of advantage in a table as there is no additional width lost by the ellipsis, and thus more content shown. But to the user it might be less obvious that there is more content without them.

Scrollbar - the block increment seems variable not fixed

In the example below the block increment of the scrollbar is set to 100. However it appears that the actual value will be a value between 1 - 100 depending on where you click on the track bar.
When running the example try clicking roughly in the middle of the track several times to scroll down. When I do this I get output like:
ScrollBar Max: 400.0
LayoutY: -100.0
LayoutY: -200.0
LayoutY: -300.0
LayoutY: -375.0 // this varies depending on the click location
I am expecting the scroll units to change by 100 no matter where I click on the track. (ie, only a single label should be visible at a time).
Is this a common scrolling feature? If so, is there a way to turn it off?
Here is the code:
import javafx.application.Application;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.geometry.Orientation;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
public class ScrollBarSSCCE extends Application
{
#Override
public void start(Stage stage)
{
int labelWidth = 300;
int labelHeight = 100;
String[] styles =
{
"-fx-background-color: #336699;",
"-fx-background-color: #996633;",
"-fx-background-color: #ff0000;",
"-fx-background-color: #00ff00;",
"-fx-background-color: #0000ff;"
};
VBox vb = new VBox();
for (int i = 0; i < styles.length; i++)
{
Label label = new Label();
label.setPrefWidth( labelWidth );
label.setPrefHeight( labelHeight );
label.setStyle( styles[i] );
vb.getChildren().add( label );
}
ScrollBar sc = new ScrollBar();
sc.setOrientation(Orientation.VERTICAL);
sc.setLayoutX(labelWidth);
sc.setMin(0);
sc.setMax( (styles.length - 1) * labelHeight);
System.out.println("ScrollBar Max: " + sc.getMax());
sc.setPrefHeight( labelHeight );
sc.setUnitIncrement( labelHeight / 2 );
sc.setBlockIncrement( labelHeight );
sc.valueProperty().addListener(
(ObservableValue<? extends Number> ov, Number old_val, Number new_val) ->
{
double y = -new_val.doubleValue();
System.out.println("LayoutY: " + y);
vb.setLayoutY( y );
});
Group root = new Group();
root.getChildren().addAll(vb, sc);
Scene scene = new Scene(root, labelWidth+20, labelHeight);
stage.setScene(scene);
stage.setTitle("ScrollBar SSCCE");
stage.show();
}
public static void main(String[] args)
{
launch(args);
}
}
i cant give a good explanation
System.out.println("block increment: " +sc.getBlockIncrement()); is always the same, and when i tested it, it happens on both sides going down or going up, i think it has to do with the ScrollBar.adjustValue(position); so if i am to provide some sort of solution i will say adjust the Value when you most need it.
//in your value listener you can add these codes
if(new_val.doubleValue() > old_val.doubleValue()){ //going down
if(sc.getMax()-sc.getBlockIncrement() < Math.abs(y)){
sc.adjustValue(1);
}
}else{//going up
if(old_val.doubleValue() == sc.getBlockIncrement()){
sc.adjustValue(0);
}
}
i think with this you might want to set sc.setUnitIncrement(labelHeight/2); the same as sc.setBlockIncrement(labelHeight); as that code might ruing the flow of UnitIncrement
hope it hepls :)
The ScrollBar.adjustValue(position) method is responsible for this behaviour.
It basically has three steps:
determine a "position value" by using the relative location of the mouse pressed
determine the "new value" by using the block increment value (this is the value I want)
Determine whether to use the "position value" or the "new value".
Since I always want the "new value" I just removed step 3 from the adjustValue(...) method.
Here is the code for the custom ScrollBar:
ScrollBar sc = new ScrollBar()
{
#Override
public void adjustValue(double position)
{
// figure out the "value" associated with the specified position
double posValue = ((getMax() - getMin()) * Utils.clamp(0, position, 1)) + getMin();
double newValue;
if (Double.compare(posValue, getValue()) != 0)
{
if (posValue > getValue())
{
newValue = getValue() + getBlockIncrement();
}
else
{
newValue = getValue() - getBlockIncrement();
}
setValue( Utils.clamp(getMin(), newValue, getMax()) );
}
}
};

How to find out line numbers of selected text?

Assume this very small program:
1. package ex1;
2. public interface Resizable {
3. void resize();
4. }
In my editor, if I select line 2-3 using mouse and say click on a button, I want to highlight these texts and also print, which line numbers were selected exactly for the button.
I can do the highlighting part, but I don't know how to find the line numbers of highlighted texts, As I think, I should use a listener, which will detect any changes in editor.
I think I should use an action listener, which will detect when the button is pressed after selecting text blocks. But how I will know, which lines are selected exactly?
The start and end of the highlight can be taken from the caret position dot and mark respectively. These are offsets in the Document. You must then calculate the number of newlines from the start of the document until the mark/do
textArea.addCaretListener(new CaretListener() {
#Override
public void caretUpdate(CaretEvent e) {
int startLine = getLine(e.getDot());
int endLine = getLine(e.getMark());
...
}
});
private int getLine(int offset) {
String text = textArea.getDocument().getText(0, offset);
int linenr = 0;
int idx = text.indexOf("\n");
while (idx != -1) {
linenr++;
idx = text.indexOf("\n", idx);
}
return linenr;
}

Access cell of a column using a given index?

I have the index (as integer) of a cell in a TableColumn of a TableView. Using the index of the cell, how can I access the cell object at this index? I am missing something like
tableColumn.getCells()
which would then allow me to access the cell with the given index:
tableColumn.getCells().get(index)
Any hints on this? Thanks!
Update:
As per request, a use case for this:
Imagine a music playlist. For some reasons I am using a TableView for that and the TableView is as big as possible, meaning even if there are just 5 music tracks in the playlist and there is space for 10 rows, then 10 rows (5 filled, 5 empty) are shown.
I then implemented drag & drop for re-arranging items. Now imagine a user wants to place the second entry to the end of the playlist. He drags the second entry, but not directly over the last used table row, but on e.g. row 8. In that case I want to give a visual feedback that the music track is placed under the last used row (row 5, but the mouse is over row/cell 8!), so I need to modify row/cell 5, while the mouse is over row 8. Knowing which row is the last filled one is quite easy - it's tableview.getItems().size()-1, but getting a reference to row/cell 5 is a tricky thing / my problem here.
This API doesn't exist for a reason.
For better performance FX TableView manipulates cells by itself. Cells are created and destroyed depending on currently visible part of the TableView and other parameters. Even more: cell objects can be reused to represent data from other cells under some circumstances.
So return value of tableColumn.getCells().get(index) is not determinate, thus there is no API to receive it.
All cell related logic should be implemented in the CellFactory callback.
Can you describe what kind of logic you want to implement by accessing cells?
I would set a fixed cell size and just take a guess using the mouse coordinates. I just added it to an existing example and I'm sure it's not quite right but it shows the idea.
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.chart.PieChart;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.control.cell.TextFieldTableCell;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import javafx.util.converter.NumberStringConverter;
public class ChangePie extends Application {
#Override
public void start(Stage primaryStage) {
ObservableList<PieChart.Data> pieChartData
= FXCollections.observableArrayList(
new PieChart.Data("Grapefruit", 13),
new PieChart.Data("Oranges", 25),
new PieChart.Data("Plums", 10),
new PieChart.Data("Pears", 22),
new PieChart.Data("Apples", 30));
final PieChart chart = new PieChart(pieChartData);
chart.setTitle("Imported Fruits");
final TableView<PieChart.Data> tv = new TableView(pieChartData);
tv.setEditable(true);
tv.setFixedCellSize(40);
tv.addEventHandler(MouseEvent.ANY, (MouseEvent evt) -> {
double y = 0;
int idx1 = 0, idx2 = 0;
if (evt.getEventType() == MouseEvent.MOUSE_PRESSED) {
y = evt.getY();
idx1 = tv.getSelectionModel().getSelectedIndex();
} else if (evt.getEventType() == MouseEvent.MOUSE_RELEASED) {
idx2 = (int) Math.round((evt.getY() - y) / 40);
System.out.println("insert at " + idx2);
if (idx2 > 0) {
chart.setData(null);
PieChart.Data removed = pieChartData.remove(idx1);
pieChartData.add(Math.min(idx2, pieChartData.size() - 1), removed);
}
}
});
TableColumn tc1 = new TableColumn("Name");
tc1.setPrefWidth(100);
tc1.setCellValueFactory(new PropertyValueFactory("name"));
tc1.setCellFactory(TextFieldTableCell.forTableColumn());
TableColumn tc2 = new TableColumn("Data");
tc2.setPrefWidth(100);
tc2.setCellValueFactory(new PropertyValueFactory("pieValue"));
tc2.setCellFactory(TextFieldTableCell.forTableColumn(new NumberStringConverter()));
tv.getColumns().addAll(tc1, tc2);
Scene scene = new Scene(new VBox(chart, tv));
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}

Categories