JavaFX SortedList retains instances of removed objects - java

I have a memory leak in my application. After profiling with VisualVM a found out that a SortedList in a TableView retains instances of removed objects at the end of its internal sorted array.
How should i try resolving this?
Using jdk1.8.0_141
Code to demonstrate the problem below (click the button below the table and you'll get an OutOfMemoryError eventually) :
import java.util.*;
import java.util.stream.*;
import javafx.application.Application;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.*;
import javafx.collections.transformation.*;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class SortedListTest extends Application {
public static void main(String[] args) {
launch(args);
}
int universeSize = 500;
Universe universe = createUniverse();
ObservableList<Entity> currentEntities = FXCollections.observableArrayList();
#Override
public void start(Stage primaryStage) throws Exception {
currentEntities.addAll(universe.entities);
MyTable table = new MyTable(currentEntities);
TableColumn<Entity, UUID> c1 = new TableColumn<>("C1");
c1.setCellValueFactory(c -> c.getValue().prop1);
table.getColumns().addAll(c1);
Button btn = new Button();
btn.setText(Integer.toString(universeSize));
btn.setOnAction(e -> {
universe = createUniverse();
currentEntities.setAll(universe.entities);
btn.setText(Integer.toString(universeSize));
});
primaryStage.setScene(new Scene(new VBox(table, btn)));
primaryStage.show();
}
Universe createUniverse() {
return new Universe(universeSize--);
}
static class Universe {
final ObservableList<Entity> entities;
final int size;
Universe(int size) {
this.size = size;
this.entities = FXCollections.observableArrayList(
IntStream.range(0, size)
.mapToObj(i -> new Entity(this))
.collect(Collectors.toList())
);
}
}
static class Entity implements Comparable<Entity> {
final byte[] blob = new byte[1024 * 1024];
final SimpleObjectProperty<UUID> prop1 = new SimpleObjectProperty<>(UUID.randomUUID());
final Universe universe;
final double filter = Math.random();
public Entity(Universe universe) {
this.universe = universe;
}
#Override
public int compareTo(Entity o) {
return prop1.get().compareTo(o.prop1.get());
}
}
static class MyTable extends TableView<Entity> {
private ObservableList<Entity> backingList;
private SortedList<Entity> sortedList;
public MyTable(ObservableList<Entity> items) {
super(items);
itemsProperty().addListener(o -> {
ObservableList<Entity> list = getItems();
if (getItems() != sortedList) {
setFilteredList(list);
}
});
setFilteredList(getItems());
}
private void setFilteredList(ObservableList<Entity> backingList) {
this.backingList = backingList;
Comparator<Entity> comparator = sortedList != null ? (Comparator<Entity>) sortedList.getComparator() : Comparator.naturalOrder();
sortedList = backingList.sorted(comparator);
sortedList.comparatorProperty().bind(comparatorProperty());
setItems(sortedList);
}
}
}
In real code the separate lists in the table are used for some additional functionality.

Related

How to put a ProcessIndicator in each row of a TableView and indicate task status

I'm trying to do the following in JavaFX:
Have a TableView with multiple rows.
Each row contains columns with text and one Progress/Status column.
When a specific Button is pressed, for each row of the TableView some task should be performed, one row after the other. (e.g. check some data, ...)
While this task is performed, a indeterminate ProgressIndicator shall be shown in the Status column, until the task for this row is finished, then the indicator shows as done.
When all tasks for each row are done, the button can be pressed again to reset the status and execute the tasks again.
I had found some help in this related Stackoverflow post and also here and tried to tweak this as needed but got stuck on some issues:
Currently, each ProgressIndicator for each row is displayed immediately (as indeterminate) when I run the program. How can I only activate them / make them visible for each row one after another once the button is pressed?
Pressing the button again once the fake tasks are done does not restart it. How would I have to modify / rebuild the program to make resets possible?
Does the overall approach make sense?
My current runnable code:
import java.util.Random;
import java.util.concurrent.*;
import javafx.application.Application;
import javafx.beans.property.ReadOnlyStringProperty;
import javafx.beans.property.ReadOnlyStringWrapper;
import javafx.beans.value.ObservableValue;
import javafx.concurrent.Task;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
import javafx.util.Callback;
public class ProgressIndicatorTableCellTest extends Application {
public void start(Stage primaryStage) {
TableView<TestTask> table = new TableView<>();
Random rng = new Random();
for (int i = 0; i < 3; i++) {
table.getItems().add(new TestTask(rng.nextInt(3000) + 2000, "Test"));
}
TableColumn<TestTask, String> nameCol = new TableColumn("Name");
nameCol.setCellValueFactory(new PropertyValueFactory<TestTask, String>("name"));
nameCol.setPrefWidth(75);
TableColumn<TestTask, Double> progressCol = new TableColumn("Progress");
progressCol.setCellValueFactory(new PropertyValueFactory<TestTask, Double>("progress"));
progressCol.setCellFactory(ProgressIndicatorTableCell.<TestTask>forTableColumn());
table.getColumns().addAll(nameCol, progressCol);
BorderPane root = new BorderPane();
root.setCenter(table);
Button btn = new Button("Start");
btn.setOnAction(actionEvent -> {
ExecutorService executor = Executors.newSingleThreadExecutor();
for (TestTask task : table.getItems()) {
executor.submit(task);
}
});
root.setBottom(btn);
primaryStage.setScene(new Scene(root));
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
public static class TestTask extends Task<Void> {
private final int waitTime; // milliseconds
final ReadOnlyStringWrapper name = new ReadOnlyStringWrapper();
public static final int NUM_ITERATIONS = 100;
public TestTask(int waitTime, String name) {
this.waitTime = waitTime;
this.name.set(name);
}
public ReadOnlyStringProperty nameProperty() {
return name.getReadOnlyProperty();
}
#Override
protected Void call() throws Exception {
this.updateProgress(ProgressIndicator.INDETERMINATE_PROGRESS, 1);
Thread.sleep(waitTime);
this.updateProgress(1, 1);
return null;
}
}
}
class ProgressIndicatorTableCell<S> extends TableCell<S, Double> {
public static <S> Callback<TableColumn<S, Double>, TableCell<S, Double>> forTableColumn() {
return new Callback<TableColumn<S, Double>, TableCell<S, Double>>() {
#Override
public TableCell<S, Double> call(TableColumn<S, Double> param) {
return new ProgressIndicatorTableCell<>();
}
};
}
private final ProgressIndicator progressIndicator;
private ObservableValue observable;
public ProgressIndicatorTableCell() {
this.progressIndicator = new ProgressIndicator();
setGraphic(progressIndicator);
}
#Override
public void updateItem(Double item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setGraphic(null);
} else {
progressIndicator.progressProperty().unbind();
observable = getTableColumn().getCellObservableValue(getIndex());
if (observable != null) {
progressIndicator.progressProperty().bind(observable);
} else {
progressIndicator.setProgress(item);
}
setGraphic(progressIndicator);
}
}
}
And the current output:
Here is a version that implements your first question. With this requirement, the cell is only a function of the task's state. If it's RUNNING, display an indeterminate progress indicator; if it's SUCCEEDED display a progress indicator with value 1; otherwise, display nothing.
Note the original question is very old and uses a lot of outdated code styles. I've updated accordingly.
import javafx.application.Application;
import javafx.beans.property.ReadOnlyStringProperty;
import javafx.beans.property.ReadOnlyStringWrapper;
import javafx.concurrent.Task;
import javafx.concurrent.Worker;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ProgressIndicatorTableCellTest extends Application {
public void start(Stage primaryStage) {
TableView<TestTask> table = new TableView<>();
Random rng = new Random();
for (int i = 0; i < 3; i++) {
table.getItems().add(new TestTask(rng.nextInt(3000) + 2000, "Test"));
}
TableColumn<TestTask, String> nameCol = new TableColumn<>("Name");
nameCol.setCellValueFactory(data -> data.getValue().nameProperty());
nameCol.setPrefWidth(75);
TableColumn<TestTask, Worker.State> progressCol = new TableColumn<>("Progress");
progressCol.setCellValueFactory(data -> data.getValue().stateProperty());
progressCol.setCellFactory(col -> new ProgressIndicatorTableCell<>());
table.getColumns().addAll(nameCol, progressCol);
BorderPane root = new BorderPane();
root.setCenter(table);
Button btn = new Button("Start");
btn.setOnAction(actionEvent -> {
ExecutorService executor = Executors.newSingleThreadExecutor(r -> {
Thread t = new Thread(r);
t.setDaemon(true);
return t;
});
for (TestTask task : table.getItems()) {
executor.submit(task);
}
});
root.setBottom(btn);
primaryStage.setScene(new Scene(root));
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
public static class TestTask extends Task<Void> {
private final int waitTime; // milliseconds
final ReadOnlyStringWrapper name = new ReadOnlyStringWrapper();
public static final int NUM_ITERATIONS = 100;
public TestTask(int waitTime, String name) {
this.waitTime = waitTime;
this.name.set(name);
}
public ReadOnlyStringProperty nameProperty() {
return name.getReadOnlyProperty();
}
#Override
protected Void call() throws Exception {
this.updateProgress(ProgressIndicator.INDETERMINATE_PROGRESS, 1);
Thread.sleep(waitTime);
this.updateProgress(1, 1);
return null;
}
}
}
class ProgressIndicatorTableCell<S> extends TableCell<S, Worker.State> {
private final ProgressIndicator progressIndicator = new ProgressIndicator();
#Override
protected void updateItem(Worker.State state, boolean empty) {
super.updateItem(state, empty);
if (state == Worker.State.SUCCEEDED) {
progressIndicator.setProgress(1);
setGraphic(progressIndicator);
} else if (state == Worker.State.RUNNING) {
progressIndicator.setProgress(-1);
setGraphic(progressIndicator);
} else {
setGraphic(null);
}
}
}
To allow for "restarting", you should use a Service instead of just a Task. This version will allow for a restart if the button is pressed multiple times, returning everything to the initial state before proceeding.
This version also factors the processing work out of the model class, which is desirable for properly assigning responsibilities to classes:
Item.java:
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyStringProperty;
import javafx.beans.property.ReadOnlyStringWrapper;
import javafx.beans.property.SimpleObjectProperty;
public class Item {
public enum State {WAITING, PROCESSING, READY}
final ReadOnlyStringWrapper name = new ReadOnlyStringWrapper();
private final ObjectProperty<State> state = new SimpleObjectProperty<>(State.WAITING);
public Item(String name) {
this.name.set(name);
}
public ReadOnlyStringProperty nameProperty() {
return name.getReadOnlyProperty();
}
public State getState() {
return state.get();
}
public ObjectProperty<State> stateProperty() {
return state;
}
public void setState(State state) {
this.state.set(state);
}
}
ProcessManager.java:
import javafx.application.Platform;
import javafx.concurrent.Service;
import javafx.concurrent.Task;
import java.util.List;
import java.util.Random;
public class ProcessManager {
private final List<Item> items;
private Random rng = new Random();
private Service<Void> service = new Service<>() {
#Override
protected Task<Void> createTask() {
return new Task<>() {
#Override
protected Void call() throws Exception {
for (Item task: items) {
try {
Platform.runLater(() -> task.setState(Item.State.PROCESSING));
Thread.sleep(2000 + rng.nextInt(3000));
Platform.runLater(() -> task.setState(Item.State.READY));
} catch (InterruptedException exc) {
Thread.currentThread().interrupt();
}
if (isCancelled()) {
Platform.runLater(() -> task.setState(Item.State.WAITING));
break;
}
}
return null;
}
};
}
};
public ProcessManager(List<Item> items) {
this.items = items ;
service.setOnCancelled(e -> items.forEach(task -> task.setState(Item.State.WAITING)));
}
public void process() {
service.restart();
}
}
and the application:
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class ProgressIndicatorTableCellTest extends Application {
public void start(Stage primaryStage) {
ObservableList<Item> tasks = FXCollections.observableArrayList();
ProcessManager processManager = new ProcessManager(tasks);
TableView<Item> table = new TableView<>();
for (int i = 0; i < 3; i++) {
Item task = new Item("Item " + (i + 1));
tasks.add(task);
}
table.setItems(tasks);
TableColumn<Item, String> nameCol = new TableColumn<>("Name");
nameCol.setCellValueFactory(data -> data.getValue().nameProperty());
nameCol.setPrefWidth(75);
TableColumn<Item, Item.State> progressCol = new TableColumn<>("Progress");
progressCol.setCellValueFactory(data -> data.getValue().stateProperty());
progressCol.setCellFactory(col -> new TableCell<>() {
private final ProgressIndicator indicator = new ProgressIndicator();
#Override
protected void updateItem(Item.State state, boolean empty) {
super.updateItem(state, empty);
if (state == Item.State.PROCESSING) {
indicator.setProgress(-1);
setGraphic(indicator);
} else if (state == Item.State.READY) {
indicator.setProgress(1);
setGraphic(indicator);
} else {
setGraphic(null);
}
}
});
table.getColumns().addAll(nameCol, progressCol);
BorderPane root = new BorderPane();
root.setCenter(table);
Button btn = new Button("Start");
btn.setOnAction(actionEvent -> processManager.process());
root.setBottom(btn);
primaryStage.setScene(new Scene(root));
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}

How to center the text in TreeTableView Column?

I've a JFXTreeTableView and i want to center the text of the data for each column.
there is one of my creating columns code :
JFXTreeTableColumn<TableData, String> DrinkColumn = new JFXTreeTableColumn<>("Drink");
DrinkColumn.setPrefWidth(100);
DrinkColumn.setCellValueFactory(new Callback<TreeTableColumn.CellDataFeatures<TableData, String>, ObservableValue<String>>() {
#Override
public ObservableValue<String> call(TreeTableColumn.CellDataFeatures<TableData, String> param) {
return param.getValue().getValue().Drink;
}
}
);
I don't use JFoenix, but using a standard TreeTableView, the following external CSS will center the text in tree table cells:
.tree-table-cell {
-fx-alignment: center ;
}
Here's a SSCCE (the code above goes in style.css):
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import javafx.application.Application;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.scene.Scene;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeTableColumn;
import javafx.scene.control.TreeTableView;
import javafx.stage.Stage;
public class TreeTableViewTest extends Application {
#Override
public void start(Stage primaryStage) {
TreeTableView<Item> table = new TreeTableView<>();
TreeTableColumn<Item, String> col = new TreeTableColumn<>("Item");
col.setCellValueFactory(cellData -> cellData.getValue().getValue().nameProperty());
col.setPrefWidth(250);
table.getColumns().add(col);
TreeTableColumn<Item, Number> valueCol = new TreeTableColumn<>("Value");
valueCol.setCellValueFactory(cellData -> cellData.getValue().getValue().valueProperty());
valueCol.setPrefWidth(150);
table.getColumns().add(valueCol);
table.setRoot(createRandomTree(50));
Scene scene = new Scene(table);
scene.getStylesheets().add("style.css");
primaryStage.setScene(scene);
primaryStage.show();
}
private TreeItem<Item> createRandomTree(int nItems) {
Random rng = new Random();
TreeItem<Item> root = new TreeItem<>(new Item("Item 1", rng.nextInt(1000)));
root.setExpanded(true);
List<TreeItem<Item>> items = new ArrayList<>();
items.add(root);
for (int i = 2 ; i <= nItems ; i++) {
TreeItem<Item> item = new TreeItem<>(new Item("Item "+i, rng.nextInt(1000)));
item.setExpanded(true);
items.get(rng.nextInt(items.size())).getChildren().add(item);
items.add(item);
}
return root ;
}
public static class Item {
private final StringProperty name = new SimpleStringProperty();
private final IntegerProperty value = new SimpleIntegerProperty();
public Item(String name, int value) {
setName(name);
setValue(value);
}
public final StringProperty nameProperty() {
return this.name;
}
public final String getName() {
return this.nameProperty().get();
}
public final void setName(final String name) {
this.nameProperty().set(name);
}
public final IntegerProperty valueProperty() {
return this.value;
}
public final int getValue() {
return this.valueProperty().get();
}
public final void setValue(final int value) {
this.valueProperty().set(value);
}
}
public static void main(String[] args) {
launch(args);
}
}
If you want to center only specific columns, then use a cell factory on the column and set a CSS class or PseudoClass on the cell:
valueCol.setCellFactory(column -> {
TreeTableCell<Item, Number> cell = new TreeTableCell<Item, Number>() {
#Override
protected void updateItem(Number value, boolean empty) {
super.updateItem(value, empty);
if (empty) {
setText(null);
} else {
setText(value.toString());
}
}
};
cell.pseudoClassStateChanged(PseudoClass.getPseudoClass("centered"), true);
return cell ;
});
and modify the CSS accordingly:
.tree-table-cell:centered {
-fx-alignment: center ;
}
The latter version gives

JavaFX ContextMenu items not displaying changes during runtime

So I am writing an AutosuggestMenu that adds a listener to a TextField and recommends suggestions in a popup ContextMenu, based on comparing the keystrokes entered with the Collection of words provided.
Unfortunately, changes to the ContextMenu elements are not displayed, and I suspect this is because I am modifying the elements of the ObservableList associated with the ContextMenu and not the list itself.
Browsing stack has led to believe I should implement an extractor, but based on the examples provided I have no idea how to do this for my specific problem. Any solution would be very much appreciated!
Source:
package com.sknb.gui;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
import javafx.geometry.Side;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.CustomMenuItem;
import javafx.scene.control.Label;
import javafx.scene.control.MenuItem;
import javafx.scene.control.TextField;
import javafx.scene.paint.Color;
import javafx.scene.text.Font;
import javafx.scene.text.FontWeight;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
public class AutosuggestMenu {
public final static int DEFAULT_MENU_SIZE = 10;
//Data members
private int menuSize;
private Collection<String> wordList;
//GUI members
private ContextMenu menu;
private final Text textBefore, textMatching, textAfter;
public AutosuggestMenu(Collection<String> keyList) {
this(keyList, DEFAULT_MENU_SIZE);
}
public AutosuggestMenu(Collection<String> keyList, int numEntries) {
if (keyList == null) {
throw new NullPointerException();
}
this.wordList = keyList;
this.menuSize = numEntries;
this.menu = new ContextMenu();
for (int i = 0; i < this.menuSize; i++) {
CustomMenuItem item = new CustomMenuItem(new Label(), true);
this.menu.getItems().add(item);
}
this.textBefore = new Text();
this.textMatching = new Text();
this.textAfter = new Text();
}
public void addListener(TextField field) {
field.textProperty().addListener((observable, oldValue, newValue) -> {
String enteredText = field.getText();
if (enteredText == null || enteredText.isEmpty()) {
this.menu.hide();
} else {
List<String> filteredEntries = this.wordList.stream()
.filter(e -> e.contains(enteredText))
.collect(Collectors.toList());
if (!filteredEntries.isEmpty()) {
populatePopup(field, filteredEntries, enteredText);
if (!(this.menu.isShowing())) {
this.menu.show(field, Side.BOTTOM, 0, 0);
}
} else {
this.menu.hide();
}
}
});
field.focusedProperty().addListener((observableValue, oldValue, newValue) -> {
this.menu.hide();
});
}
private void populatePopup(TextField field, List<String> matches, String query) {
int i = 0,
max = (matches.size() > this.menuSize) ? this.menuSize :
matches.size();
for (MenuItem item : this.menu.getItems()) {
if (i < max) {
String result = matches.get(i);
item.setGraphic(generateTextFlow(result, query));
item.setVisible(true);
item.setOnAction(actionEvent -> {
field.setText(result);
field.positionCaret(result.length());
this.menu.hide();
});
} else {
item.setVisible(false);
}
i++;
}
}
private TextFlow generateTextFlow(String text, String filter) {
int filterIndex = text.indexOf(filter);
this.textBefore.setText(text.substring(0, filterIndex));
this.textAfter.setText(text.substring(filterIndex + filter.length()));
this.textMatching.setText(text.substring(filterIndex, filterIndex + filter.length()));
textMatching.setFill(Color.BLUE);
textMatching.setFont(Font.font("Helvetica", FontWeight.BOLD, 12));
return new TextFlow(textBefore, textMatching, textAfter);
}
public int getMenuSize() {
return this.menuSize;
}
public void setMenuSize(int size) {
this.menuSize = size;
}
public Collection<String> getKeyList() {
return this.wordList;
}
public void setKeyList(Collection<String> keyList) {
this.wordList = keyList;
}
//To do: add ways to change style of ContextMenu/menu items/text elements
}
Test class using a text file of English words as a dictionary:
package autosuggestfieldtest;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Collection;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.TextField;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
import com.sknb.gui.AutosuggestMenu;
public class AutosuggestFieldTest extends Application {
private TextField textfield;
private Collection<String> words;
private AutosuggestMenu popup;
#Override
public void start(Stage primaryStage) {
String filename = Paths.get("").toAbsolutePath().toString() + "\\words.txt";
try (Stream<String> stream = Files.lines(Paths.get(filename))) {
words = stream
.collect(Collectors.toSet());
} catch (IOException e) {
System.out.println("DERP");
}
popup = new AutosuggestMenu(words);
textfield = new TextField();
popup.addListener(textfield);
StackPane root = new StackPane();
root.getChildren().add(textfield);
Scene scene = new Scene(root, 300, 250);
primaryStage.setTitle("AutocompleteField Test");
primaryStage.setScene(scene);
primaryStage.show();
}
/**
* #param args the command line arguments
*/
public static void main(String[] args) {
launch(args);
}
}
Note: this is a modification of a similar solution by Ruslan, but I was wondering if there is a solution that does not involve clearing/repopulating the menu with every keystroke? I.e. just using the setGraphic and refreshing the ContextMenu?

JavaFX format SimpleLongProperty in TableView

I'm stuck with trying to format Long values in a TableView with JavaFX.
I have following class to store the rows that I want to display on the table:
import java.text.DecimalFormat;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleLongProperty;
import javafx.beans.property.SimpleStringProperty;
public class DataByCurrencyPairRow {
private DecimalFormat integerFormat = new DecimalFormat("#,###");
private SimpleStringProperty currencyPair = new SimpleStringProperty("");
private SimpleDoubleProperty shareOfTotalVolume = new SimpleDoubleProperty(0);
private SimpleLongProperty totalVolume = new SimpleLongProperty(0);
private SimpleLongProperty currencyBought = new SimpleLongProperty(0);
private SimpleLongProperty currencySold = new SimpleLongProperty(0);
private SimpleLongProperty monthlyAverage = new SimpleLongProperty(0);
public DataByCurrencyPairRow() {
currencyPair.set("");
shareOfTotalVolume.set(0);
totalVolume.set(0);
currencyBought.set(0);
currencySold.set(0);
monthlyAverage.set(0);
}
public String getCurrencyPair() {
return currencyPair.getValue();
}
public void setCurrencyPair(String currencyPair) {
this.currencyPair.setValue(currencyPair);
}
public Long getMonthlyAverage() {
return monthlyAverage.getValue();
}
public void setMonthlyAverage(Long monthlyAverage) {
this.monthlyAverage.setValue(monthlyAverage);
}
public Long getCurrencySold() {
return currencySold.getValue();
}
public void setCurrencySold(Long currencySold) {
this.currencySold.setValue(currencySold);
}
public Long getCurrencyBought() {
return currencyBought.getValue();
}
public void setCurrencyBought(Long currencyBought) {
this.currencyBought.setValue(currencyBought);
}
public Long getTotalVolume() {
return totalVolume.getValue();
}
public void setTotalVolume(Long totalVolume) {
this.totalVolume.setValue(totalVolume);
}
public Double getShareOfTotalVolume() {
return shareOfTotalVolume.getValue();
}
public void setShareOfTotalVolume(Double shareOfTotalVolume) {
this.shareOfTotalVolume.setValue(shareOfTotalVolume);
}
}
Then I have the controller with initialize method where I have been trying to override the updateItem method to get the table to show comma as a thousand separator:
public class MainController {
private static final String DEFAULT_TIME_HORIZON = new String("0");
private final NumberFormat integerFormat = new DecimalFormat("#,###");
#FXML
TableView<DataByCurrencyPairRow> tableTransactionsByCurrencyPair;
#FXML
TableColumn<DataByCurrencyPairRow, Long> columnTotal;
#FXML
void initialize() {
columnTotal.setCellFactory(
new Callback<TableColumn<DataByCurrencyPairRow, SimpleLongProperty>, TableCell<DataByCurrencyPairRow, SimpleLongProperty>>() {
#Override
public TableCell<DataByCurrencyPairRow, SimpleLongProperty> call(TableColumn<DataByCurrencyPairRow, SimpleLongProperty> param
) {
return new TableCell<DataByCurrencyPairRow, SimpleLongProperty>() {
#Override
protected void updateItem(SimpleLongProperty item, boolean empty) {
super.updateItem(item, empty);
if (item == null || empty) {
setText("0");
setStyle("");
} else {
setText(integerFormat.format(item.longValue()));
}
}
};
}
}
);
And this is the method that populates the TableView:
public void updateByCurrencyPairTable() {
System.out.println("#MainController: Updating data in table view Markets volumes by currency pair");
ObservableList<DataByCurrencyPairRow> data = tableTransactionsByCurrencyPair.getItems();
data.clear();
// Add row items to the table view Markets volume by currency
for (DataByCurrencyPairRow row : customer.getDataByCurrencyPairR12m().getDataByCurrencyPair()) {
data.add(row);
}
}
Please help me by showing how to do this!! I also tried to override the updateItem method as Long instead of SimpleLongProperty and my IDE accepted the code but still the number is not formatted in the table.
Thank you guys in advance!!!
LongProperty implements ObservableValue<Number>, not ObservableValue<Long> (or ObservableValue<SimpleLongProperty>). So your table columns need to be of type TableColumn<DataByCurrencyPair, Number> and your cell factory needs to match those types accordingly.
Here's a simple example of a formatted column with Longs:
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.util.Random;
import javafx.application.Application;
import javafx.beans.property.LongProperty;
import javafx.beans.property.SimpleLongProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.scene.Scene;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.stage.Stage;
public class TableWithFormattedLong extends Application {
private final NumberFormat integerFormat = new DecimalFormat("#,###");
#Override
public void start(Stage primaryStage) {
TableView<Item> table = new TableView<>();
TableColumn<Item, String> itemColumn = new TableColumn<>("Item");
itemColumn.setCellValueFactory(cellData -> cellData.getValue().nameProperty());
TableColumn<Item, Number> valueColumn = new TableColumn<>("Value");
valueColumn.setCellValueFactory(cellData -> cellData.getValue().valueProperty());
valueColumn.setCellFactory(tc -> new TableCell<Item, Number>() {
#Override
protected void updateItem(Number value, boolean empty) {
super.updateItem(value, empty);
if (value == null || empty) {
setText("");
} else {
setText(integerFormat.format(value));
}
}
});
table.getColumns().add(itemColumn);
table.getColumns().add(valueColumn);
Random rng = new Random();
for (int i = 1 ; i <= 20 ; i++) {
table.getItems().add(new Item("Item "+i, rng.nextLong()));
}
primaryStage.setScene(new Scene(table, 600, 600));
primaryStage.show();
}
public static class Item {
private final StringProperty name = new SimpleStringProperty();
private final LongProperty value = new SimpleLongProperty();
public Item(String name, long value) {
setName(name);
setValue(value);
}
public final StringProperty nameProperty() {
return this.name;
}
public final String getName() {
return this.nameProperty().get();
}
public final void setName(final String name) {
this.nameProperty().set(name);
}
public final LongProperty valueProperty() {
return this.value;
}
public final long getValue() {
return this.valueProperty().get();
}
public final void setValue(final long value) {
this.valueProperty().set(value);
}
}
public static void main(String[] args) {
launch(args);
}
}
There is no need to set the Cellfactory, just set the CellValueFactory.
TableColumn<DataByCurrencyPairRow, String> columnTotal;
columnTotal.setCellValueFactory(new Callback<TableColumn.CellDataFeatures<DataByCurrencyPairRow,String>, ObservableValue<String>>() {
#Override
public ObservableValue<String> call(CellDataFeatures<DataByCurrencyPairRow, String> param) {
DataByCurrencyPairRow value = param.getValue();
return new ReadOnlyStringWrapper(NumberFormat.getNumberInstance(Locale.US).format(123123)); //replace the number with the calculated total
}
});

Countdown timer in javafx tableview

I am struggling to make a countdown timer in javaFX. I want the value of secondenColumn to be used as a timer. So for example when i add row with 'seconden'=200. The timer has to run for 200 seconds (until 0). I don't know how to begin with the code for the timer. This is what i have at the moment...
import java.util.regex.Pattern;
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import javafx.beans.property.IntegerProperty;
import javafx.collections.ListChangeListener.Change;
import javafx.animation.Timeline;
import javafx.animation.KeyFrame;
import javafx.util.Duration;
public class RuniteOre extends Application {
Stage window;
TableView<Product> table;
TextField rockInput, worldInput, aantalSpelers;
int seconden;
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) throws Exception {
window = primaryStage;
window.setTitle("Runite Ore - Calculator");
//Rock column
TableColumn<Product, String> rockColumn = new TableColumn<>("Rock");
rockColumn.setMinWidth(200);
rockColumn.setCellValueFactory(new PropertyValueFactory<>("rock"));
//World column
TableColumn<Product, Integer> worldColumn = new TableColumn<>("World");
worldColumn.setMinWidth(100);
worldColumn.setCellValueFactory(new PropertyValueFactory<>("world"));
//Aantal spelers column
TableColumn<Product, Integer> aantalSpelersColumn = new TableColumn<>("Aantal Spelers");
aantalSpelersColumn.setMinWidth(100);
aantalSpelersColumn.setCellValueFactory(new PropertyValueFactory<>("aantalSpelers"));
//Seconden column
//TableColumn<Product, Integer> secondenColumn = new TableColumn<>("Seconden");
//secondenColumn.setMinWidth(200);
//secondenColumn.setCellValueFactory(new PropertyValueFactory<>("seconden"));
TableView<Product> table = new TableView<>();
TableColumn<Product, Integer> secondenColumn = new TableColumn<>("Seconden");
table.getColumns().add(secondenColumn);
secondenColumn.setCellValueFactory(cellData -> cellData.getValue().secondsProperty().asObject());
table.getItems().addListener((Change<? extends Product> c) -> {
while (c.next()) {
if (c.wasAdded()) {
for (Product item : c.getAddedSubList()) {
int startValue = item.getSeconden() ;
Timeline countdown = new Timeline(new KeyFrame(Duration.seconds(1), e ->
item.setSeconden(item.getSeconden() - 1)
));
countdown.setCycleCount(startValue);
countdown.play();
}
}
}
});
//Rock input
rockInput = new TextField();
rockInput.setPromptText("Rocks");
rockInput.setMinWidth(100);
//World input
worldInput= new TextField();
worldInput.setPromptText("World");
//Aantal spelers input
aantalSpelers = new TextField();
aantalSpelers.setPromptText("Aantal Spelers");
//Button
Button addButton = new Button("Add");
addButton.setOnAction(e -> addButtonClicked());
Button deleteButton = new Button("Delete");
deleteButton.setOnAction(e -> deleteButtonClicked());
HBox hBox = new HBox();
hBox.setPadding(new Insets(10,10,10,10));
hBox.setSpacing(10);
hBox.getChildren().addAll(rockInput, worldInput, aantalSpelers, addButton, deleteButton);
table = new TableView<>();
table.getColumns().addAll(rockColumn, worldColumn, aantalSpelersColumn,secondenColumn);
VBox vBox = new VBox();
vBox.getChildren().addAll(table, hBox);
Scene scene = new Scene(vBox);
window.setScene(scene);
window.show();
}
//Add button clicked
public void addButtonClicked(){
Product product = new Product();
product.setRock(rockInput.getText());
product.setWorld(Integer.parseInt(worldInput.getText()));
product.setAantalSpelers(Integer.parseInt(aantalSpelers.getText()));
//TESTBEREKENING seconden=(Integer.parseInt(aantalSpelers.getText())*10);
seconden=(Integer.parseInt(aantalSpelers.getText())*10);
product.setSeconden(seconden);
table.getItems().add(product);
rockInput.clear();
worldInput.clear();
aantalSpelers.clear();
}
//Delete button clicked
public void deleteButtonClicked(){
ObservableList<Product> productSelected, allProducts;
allProducts = table.getItems();
productSelected = table.getSelectionModel().getSelectedItems();
productSelected.forEach(allProducts::remove);
}
}
and this is the code from class Product:
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
public class Product {
private String rock;
private int world;
private int aantalSpelers;
//private int seconden;
private int timer;
private final IntegerProperty seconden = new SimpleIntegerProperty() ;
public Product(){
this.rock = "";
this.world = 0;
this.aantalSpelers = 0;
}
public Product(String rock, int world, int aantalSpelers){
this.rock = rock;
this.world = world;
this.aantalSpelers = aantalSpelers;
}
public String getRock() {
return rock;
}
public void setRock(String rock) {
this.rock = rock;
}
public int getWorld() {
return world;
}
public void setWorld(int world) {
this.world = world;
}
public int getAantalSpelers() {
return aantalSpelers;
}
public void setAantalSpelers(int aantalSpelers) {
this.aantalSpelers = aantalSpelers;
}
public final int getSeconden() {
return secondsProperty().get();
}
public final void setSeconden(int seconden) {
secondsProperty().set(seconden);
}
public int getTimer() {
return timer;
}
public void setTimer(int timer) {
this.timer = timer;
}
public Product(int seconden) {
setSeconden(seconden);
}
public IntegerProperty secondsProperty() {
return seconden ;
}
}
Just create a Timeline and decrease the value once per second every time a new row is added to the table:
public void start(Stage primaryStage) throws Exception {
// existing code...
// this just needs to be executed before any rows are added to the table:
table.getItems().addListener((Change<? extends Product> c) -> {
while (c.next()) {
if (c.wasAdded()) {
for (Product p : c.getAddedSubList()) {
int startValue = p.getSeconden();
Timeline countdown = new Timeline(new KeyFrame(Duration.seconds(1),
e -> p.setSeconden(p.getSeconden() - 1)));
countdown.setCycleCount(startValue);
countdown.play();
}
}
}
});
}
This assumes your Product class follows the JavaFX properties pattern, i.e. it has a public IntegerProperty secondenProperty() { ... } method.
Here is a SSCCE:
import java.util.regex.Pattern;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.collections.ListChangeListener.Change;
import javafx.scene.Scene;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.scene.control.TextFormatter;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
import javafx.util.Duration;
public class CountdownTable extends Application {
#Override
public void start(Stage primaryStage) throws Exception {
TableView<Item> table = new TableView<>();
TableColumn<Item, Integer> secondsCol = new TableColumn<>("Seconds");
table.getColumns().add(secondsCol);
secondsCol.setCellValueFactory(cellData -> cellData.getValue().secondsProperty().asObject());
table.getItems().addListener((Change<? extends Item> c) -> {
while (c.next()) {
if (c.wasAdded()) {
for (Item item : c.getAddedSubList()) {
int startValue = item.getSeconds() ;
Timeline countdown = new Timeline(new KeyFrame(Duration.seconds(1), e ->
item.setSeconds(item.getSeconds() - 1)
));
countdown.setCycleCount(startValue);
countdown.play();
}
}
}
});
TextField textField = new TextField();
textField.setPromptText("Type a time in seconds and press enter");
Pattern integerPattern = Pattern.compile("\\d*");
TextFormatter<Integer> formatter = new TextFormatter<Integer>( (TextFormatter.Change c) -> {
String newText = c.getControlNewText();
if (integerPattern.matcher(newText).matches()) {
return c ;
} else {
return null ;
}
});
textField.setTextFormatter(formatter);
textField.setOnAction(e -> {
if (! textField.getText().isEmpty())
table.getItems().add(new Item(Integer.parseInt(textField.getText())));
textField.clear();
});
BorderPane root = new BorderPane(table, null, null, textField, null);
primaryStage.setScene(new Scene(root, 600, 600));
primaryStage.show();
}
public static class Item {
private final IntegerProperty seconds = new SimpleIntegerProperty() ;
public Item(int seconds) {
setSeconds(seconds);
}
public IntegerProperty secondsProperty() {
return seconds ;
}
public final int getSeconds() {
return secondsProperty().get();
}
public final void setSeconds(int seconds) {
secondsProperty().set(seconds);
}
}
public static void main(String[] args) { launch(args); }
}

Categories