Reputation: 1432
The scenario i am trying to achieve is,
TableCell
in a TableRow
gets updated , the row color will be changed to red and after 3 seconds the color should be automatically reverted to original. below is MCVE,
Main Class
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.stage.Stage;
public class TestProjectWin10 extends Application {
private final ObservableList<Element> data = FXCollections.observableArrayList();
public final Runnable changeValues = () -> {
int i = 0;
while (i <= 100000) {
if (Thread.currentThread().isInterrupted()) {
break;
}
data.get(0).setOccurence(System.currentTimeMillis());
data.get(0).count();
i = i + 1;
}
};
private final ExecutorService executor = Executors.newSingleThreadExecutor(runnable -> {
Thread t = new Thread(runnable);
t.setDaemon(true);
return t;
});
@Override
public void start(Stage primaryStage) {
TableView<Element> table = new TableView<>();
table.getStylesheets().add(this.getClass().getResource("tableColor.css").toExternalForm());
table.setEditable(true);
TableColumn<Element, String> nameCol = new TableColumn<>("Name");
nameCol.setPrefWidth(200);
nameCol.setCellValueFactory(cell -> cell.getValue().nameProperty());
nameCol.setCellFactory((TableColumn<Element, String> param) -> new ColorCounterTableCellRenderer(table));
table.getColumns().add(nameCol);
this.data.add(new Element());
table.setItems(this.data);
this.executor.submit(this.changeValues);
Scene scene = new Scene(table, 600, 600);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
The Element Class:
import java.util.concurrent.atomic.AtomicReference;
import javafx.application.Platform;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
public class Element {
int x = 0;
private final StringProperty nameProperty = new SimpleStringProperty("");
private final AtomicReference<String> name = new AtomicReference<>();
private final DoubleProperty occurence = new SimpleDoubleProperty();
public void count() {
x = x + 1;
if (name.getAndSet(Integer.toString(x)) == null) {
Platform.runLater(() -> nameProperty.set(name.getAndSet(null)));
}
}
public void setOccurence(double value) {
occurence.set(value);
}
public String getName() {
return nameProperty().get();
}
public void setName(String name) {
nameProperty().set(name);
}
public StringProperty nameProperty() {
return nameProperty;
}
double getOccurrenceTime() {
return occurence.get();
}
}
CellFactory code:
import java.util.Timer;
import java.util.TimerTask;
import javafx.application.Platform;
import javafx.geometry.Pos;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableRow;
import javafx.scene.control.TableView;
public class ColorCounterTableCellRenderer extends TableCell<Element, String> {
private final static long MAX_MARKED_TIME = 3000;
private final static long UPDATE_INTERVAL = 1000;
private static Timer t = null;
private final String highlightedStyle = "highlightedRow";
private final TableView tv;
public ColorCounterTableCellRenderer(TableView tv) {
this.tv = tv;
createTimer();
setAlignment(Pos.CENTER_RIGHT);
}
private void createTimer() {
if (t == null) {
t = new Timer("Hightlight", true);
t.schedule(new TimerTask() {
@Override
public void run() {
final long currentTime = System.currentTimeMillis();
TableRow tr = getTableRow();
if (tr.getItem() != null) {
if (currentTime - ((Element) tr.getItem()).getOccurrenceTime() > MAX_MARKED_TIME) {
Platform.runLater(() -> {
tr.getStyleClass().remove(highlightedStyle);
});
}
}
}
}, 0, UPDATE_INTERVAL);
}
}
@Override
protected void updateItem(String item, boolean empty) {
if (empty || getTableRow() == null || getTableRow().getItem() == null) {
setText(null);
return;
}
long currentTime = System.currentTimeMillis();
TableRow<Element> row = getTableRow();
Element elementRow = row.getItem();
double occurrenceTime = elementRow.getOccurrenceTime();
if (currentTime - occurrenceTime < MAX_MARKED_TIME) {
if (!row.getStyleClass().contains(highlightedStyle)) {
row.getStyleClass().add(highlightedStyle);
}
}
super.updateItem(item, empty);
setText(item + "");
}
}
and the css file tableColor.css
.highlightedRow {
-fx-background-color: rgba(255,0,0, 0.25);
-fx-background-insets: 0, 1, 2;
-fx-background: -fx-accent;
-fx-text-fill: -fx-selection-bar-text;
}
What is the issue..?
I check whether difference between the current time and update happened time is less than 3 seconds - row color to red (in ColorCounterTableCellRenderer
- updateItem
method)
in a separate timer (ColorCounterTableCellRenderer
) , i try check whether difference between current time and update happened time is more than 3 seconds - Removing red color.
But in the timer (createTimer
- method) code : the tr.getItem()
is always null
and hence not removing red color.
Is this the correct way to achieve what i want? Why tr.getItem()
returns null
.
To test : I ran the code and waited for executor
code to end and checked whether red color is removed after 3 seconds.
Upvotes: 5
Views: 4245
Reputation: 9914
I agree with @kleopatra comment. You cannot do complex data processing inside a cell. Mostly your Row/Cell/updateItem() should focus more on "how/what to render'. I can suggest you some key directions that you can look at.
If you want to update your row styling, based on some update in an item (not because of add/remove item, but because of an update within a property on an item) you have to first listen for the changes.
Setting a ListChangeListener on an ObservableList alone will not notify you for any changes happened 'within' properties. You have to register your ObservableList with the properties which you are interested and need to be notified when updated. So the ObservableList that you are going to set in the TableView should be registered/declared something like below::
ObservableList<Person> persons = FXCollections.observableArrayList(e ->
new Observable[]{e.pointsProperty()});
The above code forces to fire ListChangeListener of the ObservableList when pointsProperty is updated. From there you can perform what you needed.
Below is a quick working demo to highlight the row for 3seconds on every update of Points columns. I hope this demo can give you some inputs to solve your problem. This one of the approach. There can be more better ways to achieve the below feature. You can still go with @fabian solution.
Note: Use the same css file what you provided.
import javafx.animation.Animation;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.beans.Observable;
import javafx.beans.property.*;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableRow;
import javafx.scene.control.TableView;
import javafx.stage.Stage;
import javafx.util.Callback;
import javafx.util.Duration;
import java.security.SecureRandom;
public class TableRowUpdateHighlightDemo extends Application {
private final SecureRandom rnd = new SecureRandom();
@Override
public void start(Stage primaryStage) throws Exception {
ObservableList<Person> persons = FXCollections.observableArrayList(e -> new Observable[]{e.pointsProperty()});
persons.add(new Person("Harry", "John"));
persons.add(new Person("Mary", "King"));
persons.add(new Person("Don", "Bon"));
persons.add(new Person("Pink", "Wink"));
TableView<Person> tableView = new TableView<>();
TableColumn<Person, String> fnCol = new TableColumn<>("First Name");
fnCol.setCellValueFactory(param -> param.getValue().firstNameProperty());
TableColumn<Person, String> lnCol = new TableColumn<>("Last Name");
lnCol.setCellValueFactory(param -> param.getValue().lastNameProperty());
TableColumn<Person, Integer> pointsCol = new TableColumn<>("Points");
pointsCol.setCellValueFactory(param -> param.getValue().pointsProperty().asObject());
tableView.getStylesheets().add(this.getClass().getResource("tableColor.css").toExternalForm());
tableView.getColumns().addAll(fnCol, lnCol, pointsCol);
tableView.setItems(persons);
tableView.getItems().addListener((ListChangeListener<Person>) c -> {
if (c.next()) {
if (c.wasUpdated()) {
tableView.getItems().get(c.getFrom()).setHightlight(true);
tableView.refresh();
}
}
});
tableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
tableView.setRowFactory(new Callback<TableView<Person>, TableRow<Person>>() {
@Override
public TableRow<Person> call(TableView<Person> param) {
return new TableRow<Person>() {
Timeline highlightTL;
@Override
protected void updateItem(Person item, boolean empty) {
super.updateItem(item, empty);
removeHighlight();
if (item != null && item.isHightlight()) {
getStyleClass().add("highlightedRow");
getHighlightTL().playFromStart();
}
}
private void removeHighlight() {
getHighlightTL().stop();
getStyleClass().removeAll("highlightedRow");
}
private Timeline getHighlightTL() {
if (highlightTL == null) {
// After 3 secs, the hightlight will be removed.
highlightTL = new Timeline(new KeyFrame(Duration.millis(3000), e -> {
getItem().setHightlight(false);
removeHighlight();
}));
highlightTL.setCycleCount(1);
}
return highlightTL;
}
};
}
});
Scene sc = new Scene(tableView);
primaryStage.setScene(sc);
primaryStage.show();
// Updating points every 5 secs to a random person.
Timeline tl = new Timeline(new KeyFrame(Duration.millis(5000), e -> {
Person p = persons.get(rnd.nextInt(4));
p.setPoints(p.getPoints() + 1);
}));
tl.setCycleCount(Animation.INDEFINITE);
tl.play();
}
class Person {
private StringProperty firstName = new SimpleStringProperty();
private StringProperty lastName = new SimpleStringProperty();
private IntegerProperty points = new SimpleIntegerProperty();
private BooleanProperty hightlight = new SimpleBooleanProperty();
public Person(String fn, String ln) {
setFirstName(fn);
setLastName(ln);
}
public String getFirstName() {
return firstName.get();
}
public StringProperty firstNameProperty() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName.set(firstName);
}
public String getLastName() {
return lastName.get();
}
public StringProperty lastNameProperty() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName.set(lastName);
}
public int getPoints() {
return points.get();
}
public IntegerProperty pointsProperty() {
return points;
}
public void setPoints(int points) {
this.points.set(points);
}
public boolean isHightlight() {
return hightlight.get();
}
public BooleanProperty hightlightProperty() {
return hightlight;
}
public void setHightlight(boolean hightlight) {
this.hightlight.set(hightlight);
}
}
}
Update :: If you can manage to update the "hightlight" property value externally (after 3secs), then there is no need of Timeline in the RowFactory. Simply calling tableView.refresh() in the ListChangeListener shoud do the trick :)
Upvotes: -1
Reputation: 82461
Any updates to the UI, even if it's triggered through listeners, needs to be done from the application thread. (You can overcome this issue by doing the updates using Platform.runLater
.)
Furthermore you cannot rely on the same cell keeping the same cell for the complete time it's supposed to be shown as marked.
To overcome this issue you need to store the info about the marked cells either in the item itself or in some observable external data structure.
The following example stores the times of the last update in a ObservableMap
and uses a AnimationTimer
to clear expired entries from the map. Furthermore it uses TableRow
s to update a pseudoclass based on the contents of the map.
private static class Item {
private final IntegerProperty value = new SimpleIntegerProperty();
}
private final ObservableMap<Item, Long> markTimes = FXCollections.observableHashMap();
private AnimationTimer updater;
private void updateValue(Item item, int newValue) {
int oldValue = item.value.get();
if (newValue != oldValue) {
item.value.set(newValue);
// update time of item being marked
markTimes.put(item, System.nanoTime());
// timer for removal of entry
updater.start();
}
}
@Override
public void start(Stage primaryStage) {
Item item = new Item(); // the item that is updated
TableView<Item> table = new TableView<>();
table.getItems().add(item);
// some additional items to make sure scrolling effects can be tested
IntStream.range(0, 100).mapToObj(i -> new Item()).forEach(table.getItems()::add);
TableColumn<Item, Number> column = new TableColumn<>();
column.getStyleClass().add("mark-column");
column.setCellValueFactory(cd -> cd.getValue().value);
table.getColumns().add(column);
final PseudoClass marked = PseudoClass.getPseudoClass("marked");
table.setRowFactory(tv -> new TableRow<Item>() {
final InvalidationListener reference = o -> {
pseudoClassStateChanged(marked, !isEmpty() && markTimes.containsKey(getItem()));
};
final WeakInvalidationListener listener = new WeakInvalidationListener(reference);
@Override
protected void updateItem(Item item, boolean empty) {
boolean wasEmpty = isEmpty();
super.updateItem(item, empty);
if (empty != wasEmpty) {
if (empty) {
markTimes.removeListener(listener);
} else {
markTimes.addListener(listener);
}
}
reference.invalidated(null);
}
});
Scene scene = new Scene(table);
scene.getStylesheets().add("style.css");
primaryStage.setScene(scene);
primaryStage.show();
updater = new AnimationTimer() {
@Override
public void handle(long now) {
for (Iterator<Map.Entry<Item, Long>> iter = markTimes.entrySet().iterator(); iter.hasNext();) {
Map.Entry<Item, Long> entry = iter.next();
if (now - entry.getValue() > 2_000_000_000L) { // remove after 1 sec
iter.remove();
}
}
// pause updates, if there are no entries left
if (markTimes.isEmpty()) {
stop();
}
}
};
final Random random = new Random();
Thread t = new Thread(() -> {
while (true) {
try {
Thread.sleep(4000);
} catch (InterruptedException ex) {
continue;
}
Platform.runLater(() -> {
updateValue(item, random.nextInt(4));
});
}
});
t.setDaemon(true);
t.start();
}
.table-row-cell:marked .table-cell.mark-column {
-fx-background-color: red;
}
Upvotes: 7