0

For some time i have been trying to get my tableview work as kind of spreadsheet that is updated by background thread and when cell get updated, it for few seconds higlights ( changes style ) and then goes back to original style. I already know, that i can't store and set styles directly in table cell and i need some kind of backing class, that will hold this data. But tableview with its "reusing" of cells (using same cells for different data) acts really weird. When all cells fits on screen it works flawlessly for me, but once i place around 100 cells and it becomes scrollable it starts being buggy, sometimes styles ( or setted graphic) disappears and after scrolling appears, if i disable some top cells of view, some other cells after scrolling get disabled as well and so on. Is there any right way to do this?

What i need basically is

Background data thread ---updates--> tableview
Another thread --after few seconds removes style--> tableview

As i have it now, i have model class that holds data, style and reference to table cell where it should be ( i disabled ordering, so it should be ok ) and background thread updates data in model class, and that model class changes style on referenced cell and register itself in "style remover" thread, that after while removes style.

I think posting my actual code won't be useful, because once i've discovered that cells are being reused my code has become too complicated and a little bit unreadable so i want to completely redo it right way.

Peformance is not that important for me, there wont be more than 100 cells, but this highlighting and having buttons in tableview must work flawlessly.

This is how my app looks like now - for idea of what i need. enter image description here

EDIT: here is link to my another question related to this.

David S.
  • 292
  • 1
  • 19
  • 1
    didn't check the code in your other question, just a beware: when using threads, make certain that all updates to the visuals happen on the FXApplication thread – kleopatra Aug 21 '17 at 08:06
  • I use Platform.runLater for every visual update. Code in the other question is not much actual anymore, but the idea i wrote there in answer is still same i use now. I'll link the other question here i guess. – David S. Aug 21 '17 at 08:10
  • I would model the recently-changed in the data, update it as appropriate, let the view (cell) listen to that property and update its style accordingly. Should be safe as long as it rewires the listener when the item/index is changed – kleopatra Aug 21 '17 at 08:44
  • That sounds like good solution, but i'm not sure how to do this. If i store styles in my model class, i need to acces it from cell, i know i can do that by calling `getTableRow().getItem()` but i'm not sure where should i call it. I originally thought that calling it in updateItem should be enough, but when i remove style i need to somehow refresh that cell and calling `updateItem(val, false)` acted weird. And i guess i will need to do same for `setGraphic(button)`. – David S. Aug 21 '17 at 09:48
  • the interplay of the updateXX methods is a bit ... ill-specified: I would try to add/remove the listener in updateIndex. And don't store styles just a flag if the value is recently changed – kleopatra Aug 21 '17 at 10:11
  • updateIndex isnt reliable ... back to square one, sry ;) – kleopatra Aug 21 '17 at 10:20
  • at the moment i think that problem might be, that i have reference for cell in my model class, but when i'm removing style, i call my checkStyle() method on the cell ferecenced in model instance. That might cause that if at the time when it should be removed is that cell used to render different data, it could cause some mess. Maybe the remover thread should only remove style from model class and i might make another thread that would have reference to all cells and periodically would call checkstyle on them, although i don't like this solution. – David S. Aug 21 '17 at 10:32
  • actually, it did work - just made a mistake in not cleaning up ;) Will try to put in into an answer ... – kleopatra Aug 21 '17 at 10:51
  • Nice, thank you. You've just saved me from implementing own spreadsheet with gridpane :D – David S. Aug 21 '17 at 10:56

1 Answers1

1

The collaborators:

  • on the data side, a (view) model which has a recentlyChanged property, that's updated whenever the value is changed
  • on the view side, a custom cell that listens to that recentlyChanged property and updates its style as appropriate

The tricky part is to clean up cell state when re-used or not-used: the method that's always (hopefully!) called is cell.updateIndex(int newIndex), so that's the place to un-/register the listener.

Below a runnable (though crude ;) example

import java.util.logging.Logger;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import de.swingempire.fx.util.FXUtils;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.ReadOnlyBooleanWrapper;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ChangeListener;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
import javafx.util.Duration;

public class TableCoreRecentlyChanged extends Application {

    public static class RecentChanged extends TableCell<Dummy, String> {

        private ChangeListener<Boolean> recentListener = (src, ov, nv) -> updateRecentStyle(nv);
        private Dummy lastDummy;

        /*
         * Just to see any effect.
         */
        protected void updateRecentStyle(boolean highlight) {
            if (highlight) {
                setStyle("-fx-background-color: #99ff99");
            } else {
                setStyle("-fx-background-color: #009900");
            }
        }

        @Override
        public void updateIndex(int index) {
            if (lastDummy != null) {
                lastDummy.recentlyChangedProperty().removeListener(recentListener);
                lastDummy = null;
            }
            updateRecentStyle(false);
            super.updateIndex(index);
            if (getTableRow() != null && getTableRow().getItem() != null) {
                lastDummy = getTableRow().getItem();
                updateRecentStyle(lastDummy.recentlyChangedProperty().get());
                lastDummy.recentlyChangedProperty().addListener(recentListener);
            } 
        }

        @Override 
        protected void updateItem(String item, boolean empty) {
            if (item == getItem()) return;

            super.updateItem(item, empty);

            if (item == null) {
                super.setText(null);
                super.setGraphic(null);
            } else {
                super.setText(item);
                super.setGraphic(null);
            }
        }

    }

    private Parent getContent() {
        TableView<Dummy> table = new TableView<>(createData(50));
        table.setEditable(true);

        TableColumn<Dummy, String> column = new TableColumn<>("Value");
        column.setCellValueFactory(c -> c.getValue().valueProperty());
        column.setCellFactory(e -> new RecentChanged());
        column.setMinWidth(200);
        table.getColumns().addAll(column);

        int editIndex = 20; 

        Button changeValue = new Button("Edit");
        changeValue.setOnAction(e -> {
            Dummy dummy = table.getItems().get(editIndex);
            dummy.setValue(dummy.getValue()+"x");
        });
        HBox buttons = new HBox(10, changeValue);
        BorderPane content = new BorderPane(table);
        content.setBottom(buttons);
        return content;
    }

    private ObservableList<Dummy> createData(int size) {
        return FXCollections.observableArrayList(
                Stream.generate(Dummy::new)
                .limit(size)
                .collect(Collectors.toList()));
    }

    private static class Dummy {
        private static int count;

        ReadOnlyBooleanWrapper recentlyChanged = new ReadOnlyBooleanWrapper() {

            Timeline recentTimer;
            @Override
            protected void invalidated() {
                if (get()) {
                    if (recentTimer == null) {
                        recentTimer = new Timeline(new KeyFrame(
                                Duration.millis(2500),
                                ae -> set(false)));
                    }
                    recentTimer.playFromStart();
                } else {
                    if (recentTimer != null) recentTimer.stop();
                }
            }

        };
        StringProperty value = new SimpleStringProperty(this, "value", "initial " + count++) {

            @Override
            protected void invalidated() {
                recentlyChanged.set(true);
            }

        };

        public StringProperty valueProperty() {return value;}
        public String getValue() {return valueProperty().get(); }
        public void setValue(String text) {valueProperty().set(text); }
        public ReadOnlyBooleanProperty recentlyChangedProperty() { return recentlyChanged.getReadOnlyProperty(); }
        public String toString() {return "[dummy: " + getValue() + "]";}
    }

    @Override
    public void start(Stage primaryStage) throws Exception {
        primaryStage.setScene(new Scene(getContent()));
     //   primaryStage.setTitle(FXUtils.version());
        primaryStage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }

    @SuppressWarnings("unused")
    private static final Logger LOG = Logger
            .getLogger(TableCoreRecentlyChanged.class.getName());
}
kleopatra
  • 51,061
  • 28
  • 99
  • 211
  • Thanks, i'll try to implement it according to this example and then mark it as answer or comment if i'll face some issues i won't be able to solve myself. – David S. Aug 21 '17 at 11:58
  • This works for me, i was able to use this in my solution and make highlighting work. You saved me a lot of time. – David S. Aug 22 '17 at 06:54
  • Only problem i've found when i used this solution is that when you run application and some cells updates, it higlights the ones that are on screen, when i scroll down, those have updated values but not styles. Once i scroll down and up again, everything works ok. I think it needs at this moment to run updateIndex for all rows to work. – David S. Aug 22 '17 at 09:08
  • hmm ... works for me, maybe a jdk version problem? mine is 9ea-u180. and don't forget: it's a POC with rough edges - f.i. the timer isn't reset if the next change happens while the current period has not yet expired (probably need to handle the timer directly, replay and bind the boolean on its status changes. – kleopatra Aug 22 '17 at 09:40
  • Well i needed to change it a little bit, so im have maybe made a bug by accident, but i tried to keep idea you posted in your code. I can post my code ( https://gist.github.com/anonymous/53ef4f45b2b4ecc1208241163538eff0 ), it is POC as well, no much exception and null checks, just to see if tableview can handle our requirements for project. – David S. Aug 22 '17 at 10:18
  • @DavidS. that's too much code for me right now, sry - have my own work to do – kleopatra Aug 22 '17 at 11:12
  • no problem, just wanted to post it if you would have sometime a while. There should not be much difference , because i've just added two properties and i did add handlers in cell class ( those handlers call updateRecentStyle as well) and i'm changing one of those properties ( that contains style that should be set ) in another thread. – David S. Aug 22 '17 at 11:22