How to get JavaFX TreeView to behave consistently upon node expansion? - javafx

I have a JavaFX TreeView with an invisible root and a handful of 'folder' TreeItems that have many 'file' TreeItems as children. The 'folder' TreeItems typically fit inside the TreeView without there being any scrollbars.
invisible-root/
folder/
folder/
folder/
file
file
file
...
file
Sometimes, when I expand a 'folder' TreeItem, the scrollbars appear but the scroll position remains the same. (This is what I want!) However, sometimes, expanding a TreeItem causes the scrollbars appear and the TableView scrolls to the last child of the expanded TreeItem!
This is very unexpected and surprising, especially since I have difficulty predicting which of the two behaviors I will see: (1) stay put, or (2) scroll to last item. Personally, I think behavior (1) is less surprising and preferable.
Any thoughts on how to deal with this?
I see this behavior on Java8u31.

The problem is in VirtualFlow. In layoutChildren() there is this section:
if (lastCellCount != cellCount) {
// The cell count has changed. We want to keep the viewport
// stable if possible. If position was 0 or 1, we want to keep
// the position in the same place. If the new cell count is >=
// the currentIndex, then we will adjust the position to be 1.
// Otherwise, our goal is to leave the index of the cell at the
// top consistent, with the same translation etc.
if (position == 0 || position == 1) {
// Update the item count
// setItemCount(cellCount);
} else if (currentIndex >= cellCount) {
setPosition(1.0f);
// setItemCount(cellCount);
} else if (firstCell != null) {
double firstCellOffset = getCellPosition(firstCell);
int firstCellIndex = getCellIndex(firstCell);
// setItemCount(cellCount);
adjustPositionToIndex(firstCellIndex);
double viewportTopToCellTop = -computeOffsetForCell(firstCellIndex);
adjustByPixelAmount(viewportTopToCellTop - firstCellOffset);
}
The problem arises if position is 1.0 (== scrolled to bottom), because in that case there is no recalculation. A workaround would be to override the TreeViewSkin to provide your own VirtualFlow and fix the behavior there.
The code below is meant to illustrate the problem, it's not a real solution, just a starting point if you really want to fix it:
import com.sun.javafx.scene.control.skin.TreeViewSkin;
import com.sun.javafx.scene.control.skin.VirtualFlow;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.IndexedCell;
import javafx.scene.control.Skin;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class TreeViewScrollBehaviour extends Application {
#Override
public void start(Stage primaryStage) {
TreeView treeView = new TreeView() {
#Override
protected Skin createDefaultSkin() {
return new TTreeViewSkin(this); //To change body of generated methods, choose Tools | Templates.
}
};
TreeItem<String> treeItem = new TreeItem<String>("Root");
for (int i = 0; i < 20; i++) {
TreeItem<String> treeItem1 = new TreeItem<>("second layer " + i);
treeItem.getChildren().add(treeItem1);
for (int j = 0; j < 20; j++) {
treeItem1.getChildren().add(new TreeItem<>("Third Layer " + j));
}
}
treeView.setRoot(treeItem);
StackPane root = new StackPane();
root.getChildren().addAll(treeView);
Scene scene = new Scene(root, 300, 250);
primaryStage.setTitle("Hello World!");
primaryStage.setScene(scene);
primaryStage.show();
}
/**
* #param args the command line arguments
*/
public static void main(String[] args) {
launch(args);
}
class TTreeViewSkin<T extends IndexedCell> extends TreeViewSkin<T> {
public TTreeViewSkin(TreeView treeView) {
super(treeView);
}
#Override
protected VirtualFlow createVirtualFlow() {
return new TVirtualFlow<T>(); //To change body of generated methods, choose Tools | Templates.
}
}
class TVirtualFlow<T extends IndexedCell> extends VirtualFlow<T> {
#Override
public double getPosition() {
double position = super.getPosition();
if (position == 1.0d) {
return 0.99999999999;
}
return super.getPosition(); //To change body of generated methods, choose Tools | Templates.
}
#Override
public void setPosition(double newPosition) {
if (newPosition == 1.0d) {
newPosition = 0.99999999999;
}
super.setPosition(newPosition); //To change body of generated methods, choose Tools | Templates.
}
}
}

Related

Delegate mouse events to all children in a JavaFX StackPane

I'm trying to come up with a solution to allow multiple Pane nodes handle mouse events independently when assembled into a StackPane
StackPane
Pane 1
Pane 2
Pane 3
I'd like to be able to handle mouse events in each child, and the first child calling consume() stops the event going to the next child.
I'm also aware of setPickOnBounds(false), but this does not solve all cases as some of the overlays will be pixel based with Canvas, i.e. not involving the scene graph.
I've tried various experiments with Node.fireEvent(). However these always lead to recursion ending in stack overflow. This is because the event is propagated from the root scene and triggers the same handler again.
What I'm looking for is some method to trigger the event handlers on the child panes individually without the event travelling through its normal path.
My best workaround so far is to capture the event with a filter and manually invoke the handler. I'd need to repeat this for MouseMoved etc
parent.addEventFilter(MouseEvent.MOUSE_CLICKED, event -> {
for (Node each : parent.getChildren()) {
if (!event.isConsumed()) {
each.getOnMouseClicked().handle(event);
}
}
event.consume();
});
However this only triggers listeners added with setOnMouseClicked, not addEventHandler, and only on that node, not child nodes.
Another sort of solution is just to accept JavaFX doesn't work like this, and restructure the panes like this, this will allow normal event propagation to take place.
Pane 1
Pane 2
Pane 3
Example
import javafx.application.Application;
import javafx.event.Event;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.CornerRadii;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
public class EventsInStackPane extends Application {
public static void main(String[] args) {
launch(args);
}
private static class DebugPane extends Pane {
public DebugPane(Color color, String name) {
setBackground(new Background(new BackgroundFill(color, CornerRadii.EMPTY, Insets.EMPTY)));
setOnMouseClicked(event -> {
System.out.println("setOnMouseClicked " + name + " " + event);
});
addEventHandler(MouseEvent.MOUSE_CLICKED, event -> {
System.out.println("addEventHandler " + name + " " + event);
});
addEventFilter(MouseEvent.MOUSE_CLICKED, event -> {
System.out.println("addEventFilter " + name + " " + event);
});
}
}
#Override
public void start(Stage primaryStage) throws Exception {
DebugPane red = new DebugPane(Color.RED, "red");
DebugPane green = new DebugPane(Color.GREEN, "green");
DebugPane blue = new DebugPane(Color.BLUE, "blue");
setBounds(red, 0, 0, 400, 400);
setBounds(green, 25, 25, 350, 350);
setBounds(blue, 50, 50, 300, 300);
StackPane parent = new StackPane(red, green, blue);
eventHandling(parent);
primaryStage.setScene(new Scene(parent));
primaryStage.show();
}
private void eventHandling(StackPane parent) {
parent.addEventHandler(MouseEvent.MOUSE_CLICKED, event -> {
if (!event.isConsumed()) {
for (Node each : parent.getChildren()) {
Event copy = event.copyFor(event.getSource(), each);
parent.fireEvent(copy);
if (copy.isConsumed()) {
break;
}
}
}
event.consume();
});
}
private void setBounds(DebugPane panel, int x, int y, int width, int height) {
panel.setLayoutX(x);
panel.setLayoutY(y);
panel.setPrefWidth(width);
panel.setPrefHeight(height);
}
}
Using the hint from #jewelsea I was able to use a custom chain. I've done this from a "catcher" Pane which is added to the front of the StackPane. This then builds a chain using all the children, in reverse order, excluding itself.
private void eventHandling(StackPane parent) {
Pane catcher = new Pane() {
#Override
public EventDispatchChain buildEventDispatchChain(EventDispatchChain tail) {
EventDispatchChain chain = super.buildEventDispatchChain(tail);
for (int i = parent.getChildren().size() - 1; i >= 0; i--) {
Node child = parent.getChildren().get(i);
if (child != this) {
chain = chain.prepend(child.getEventDispatcher());
}
}
return chain;
}
};
parent.getChildren().add(catcher);
}

Fixed size JavaFX component

Creating new components in JavaFX is still a but muddy to me compared to "Everything is a JPanel" in Swing.
I'm trying to make a fixed size component. I hesitate to call it a control, it's a pane of activity, not a button.
But here's my problem.
The fixed size I want is smaller than the contents of the element.
The grid is, in truth, 200x200. I'm shifting it up and left 25x25, and I'm trying to make the fixed size of 150x150. You can see in my example I've tried assorted ways of forcing it to 150, but in my tests, the size never sticks. Also, to be clear, I would expect the lines to clip at the boundary of the component.
This is, roughly, what I'm shooting for in my contrived case (note this looks bigger than 150x150 because of the retina display on my Mac, which doubles everything):
I've put some in to a FlowPane, and they stack right up, but ignore the 150x150 dimensions.
FlowPane fp = new FlowPane(new TestPane(), new TestPane(), new TestPane());
var scene = new Scene(fp, 640, 480);
stage.setScene(scene);
I tried sticking one in a ScrollPane, and the scroll bars never appear, even after resizing the window.
TestPane pane = new TestPane();
ScrollPane sp = new ScrollPane(pane);
var scene = new Scene(sp, 640, 480);
stage.setScene(scene);
And I struggle to discern whether I should be extending Region or Control in these cases.
I am missing something fundamental.
package pkg;
import javafx.collections.ObservableList;
import javafx.scene.Node;
import javafx.scene.control.Control;
import javafx.scene.shape.Line;
import javafx.scene.transform.Translate;
public class TestPane extends Control {
public TestPane() {
setMinHeight(150);
setMaxHeight(150);
setMinWidth(150);
setMaxWidth(150);
setPrefHeight(150);
setPrefWidth(150);
populate();
}
#Override
protected double computePrefHeight(double width) {
return 150;
}
#Override
protected double computePrefWidth(double height) {
return 150;
}
#Override
protected double computeMaxHeight(double width) {
return 150;
}
#Override
protected double computeMaxWidth(double height) {
return 150;
}
#Override
protected double computeMinHeight(double width) {
return 150;
}
#Override
protected double computeMinWidth(double height) {
return 150;
}
#Override
public boolean isResizable() {
return false;
}
private void populate() {
Translate translate = new Translate();
translate.setX(-25);
translate.setY(-25);
getTransforms().clear();
getTransforms().addAll(translate);
ObservableList<Node> children = getChildren();
for (int i = 0; i < 4; i++) {
Line line = new Line(0, i * 50, 200, i * 50);
children.add(line);
line = new Line(i * 50, 0, i * 50, 200);
children.add(line);
}
}
}
Addenda, to clarify.
I want a fixed sized component. It's a rectangle. I want it X x Y big.
I want to draw things in my box. Lines, circles, text.
I want the things I draw to clip to the boundaries of the component.
I don't want to use Canvas.
More addenda.
What I'm looking for is not much different from what a ScrollPane does, save I don't want any scroll bars, and I don't want the size of the outlying pane to grow or shrink.
TLDR:
Subclass Region,
make isResizable() return true to respect pref, min, and max sizes,
explicitly set a clip to avoid painting outside the local bounds.
Most of the documentation for this is in the package documentation for javafx.scene.layout
First, note the distinction between resizable and non-resizable nodes. Resizable nodes (for which isResizable() returns true) are resized by their parent during layout, and the parent will make a best-effort to respect their preferred, minimum, and maximum sizes.
Non-resizable nodes are not resized by their parent. If isResizable() returns false, then resize() is a no-op and the preferred, minimum, and maximum sizes are effectively ignored. Their sizes are computed internally and reported to the parent via its visual bounds. Ultimately, all JavaFX nodes have a peer node in the underlying graphical system, and AFAIK the only way a non-resizable node can determine its size is by directly setting the size of the peer. (I'm happy to be corrected on this.)
So unless you want to get your hands really dirty with custom peer nodes (and I don't even know if the API has mechanisms for this), I think the preferred way to create a "fixed size node" is by creating a resizable node with preferred, minimum, and maximum sizes all set to the same value. This is likely by design: as noted in a comment to your question, fixed-size nodes in layout-driven UI toolkits are generally discouraged, other than very low-level components (Text, Shape, etc).
Transformations applied to resizable nodes are generally applied after layout (i.e. they don't affect the layout bounds). Therefore using a translation to manage the internal positioning of the child nodes is not a good approach; it will have effects on the layout of the custom node in the parent which you probably don't intend.
As you note, you are not really defining a control here; it has no behavior or skin. Thus subclassing Control is not really the rigth approach. The most appropriate hook in the API is to subclass Region. Override the layoutChildren() method to position the child nodes (for Shapes and Text nodes, set their coordinates, for resizable children call resizeRelocate(...)).
Finally, to prevent the node spilling out of its intended bounds (150x150 in your example), either ensure no child nodes are positioned outside those bounds, or explicitly set the clip.
Here's a refactoring of your example:
import javafx.scene.layout.Region;
import javafx.scene.shape.Line;
import javafx.scene.shape.Rectangle;
public class TestPane extends Region {
private Line[] verticalLines ;
private Line[] horizontalLines ;
private static final int WIDTH = 150 ;
private static final int HEIGHT = 150 ;
private static final int LINE_GAP = 50 ;
public TestPane() {
populate();
}
#Override
protected double computePrefHeight(double width) {
return HEIGHT;
}
#Override
protected double computePrefWidth(double height) {
return HEIGHT;
}
#Override
protected double computeMaxHeight(double width) {
return HEIGHT;
}
#Override
protected double computeMaxWidth(double height) {
return WIDTH;
}
#Override
protected double computeMinHeight(double width) {
return WIDTH;
}
#Override
protected double computeMinWidth(double height) {
return WIDTH;
}
#Override
public boolean isResizable() {
return true;
}
#Override
public void layoutChildren() {
double w = getWidth();
double h = getHeight() ;
double actualWidth = verticalLines.length * LINE_GAP ;
double actualHeight = horizontalLines.length * LINE_GAP ;
double hOffset = (actualWidth - w) / 2 ;
double vOffset = (actualHeight - h) / 2 ;
for (int i = 0 ; i < verticalLines.length ; i++) {
double x = i * LINE_GAP - hOffset;
verticalLines[i].setStartX(x);
verticalLines[i].setEndX(x);
verticalLines[i].setStartY(0);
verticalLines[i].setEndY(h);
}
for (int i = 0 ; i < horizontalLines.length ; i++) {
double y = i * LINE_GAP - vOffset;
horizontalLines[i].setStartY(y);
horizontalLines[i].setEndY(y);
horizontalLines[i].setStartX(0);
horizontalLines[i].setEndX(w);
}
setClip(new Rectangle(0, 0, w, h));
}
private void populate() {
verticalLines = new Line[4] ;
horizontalLines = new Line[4] ;
for (int i = 0 ; i <verticalLines.length ; i++) {
verticalLines[i] = new Line();
getChildren().add(verticalLines[i]);
}
for (int i = 0 ; i <horizontalLines.length ; i++) {
horizontalLines[i] = new Line();
getChildren().add(horizontalLines[i]);
}
}
}
A more sophisticated example might have, for example, LINE_GAP as a property. When that property changes you would call requestLayout() to mark the component as "dirty", so its layoutChildren() method would be called again on the next frame rendered.
Here's a quick test case:
import javafx.application.Application;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.layout.FlowPane;
import javafx.stage.Stage;
public class App extends Application {
#Override
public void start(Stage stage) {
FlowPane root = new FlowPane();
root.setAlignment(Pos.TOP_LEFT);
root.setPadding(new Insets(10));
root.setHgap(5);
root.setVgap(5);
for (int i = 0; i < 6 ; i++) {
root.getChildren().add(new TestPane());
}
Scene scene = new Scene(root);
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch();
}
}
Which results in:
This plays nicely with the layout pane; resizing the window gives

SplitPane dividers synchronization JavaFX

I want to synchronize dividers in SplitPane, when divider(0) moves, I also want to make the same move by divider(1). I guess I have to bind the positionProperty of divider(0) with something.
How can I achieve this?
You need to add listeners to the positions of each divider, and update the "linked" divider when it changes. It's important to make sure you don't end up in an infinite recursive loop; the simplest way to do this is to set a flag indicating your updating, and not propagate the update if it's set.
Here's a proof-of-concept example that binds two dividers so the portion between them is always 1/3 of the split pane:
import java.util.List;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.SplitPane;
import javafx.scene.control.SplitPane.Divider;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Region;
import javafx.stage.Stage;
public class SplitPaneDemo extends Application {
// helper class that binds two divider positions so the portion between them
// is always 1/3 of the split pane
private static class DividerPositionBinder {
private static final double ONE_THIRD = 1.0/3.0;
private boolean updating ;
DividerPositionBinder(List<Divider> dividers) {
dividers.get(0).positionProperty().addListener((obs, oldPos, newPos) -> {
// don't propagate update if already in an update:
if (updating) return ;
// special handling for right edge of split pane:
if (newPos.doubleValue() > 1.0 - ONE_THIRD) {
dividers.get(0).setPosition(1.0 - ONE_THIRD);
dividers.get(1).setPosition(1.0);
return ;
}
// make right divider the new value + 1/3:
updating = true ;
dividers.get(1).setPosition(newPos.doubleValue() + ONE_THIRD);
updating = false ;
});
dividers.get(1).positionProperty().addListener((obs, oldPos, newPos) -> {
// don't propagate update if already in an update:
if (updating) return ;
// special handling for left edge of split pane:
if (newPos.doubleValue() < ONE_THIRD) {
dividers.get(1).setPosition(ONE_THIRD);
dividers.get(0).setPosition(0.0);
return ;
}
// make left divider the new value - 1/3:
updating = true ;
dividers.get(0).setPosition(newPos.doubleValue() - ONE_THIRD);
updating = false ;
});
}
}
#Override
public void start(Stage primaryStage) {
Region left = new Pane();
left.setStyle("-fx-background-color: coral; ");
Region middle = new Pane();
middle.setStyle("-fx-background-color: aquamarine ;");
Region right = new Pane();
right.setStyle("-fx-background-color: cornflowerblue ;");
SplitPane splitPane = new SplitPane(left, middle, right);
new DividerPositionBinder(splitPane.getDividers());
Scene scene = new Scene(splitPane, 800, 800);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}

TableCell.setText(String) doesn't set the data value associated with the cell

In my particular case I have a custom implementation of a TableCell that contains a Button. This button invokes a method that returns a String to be displayed instead of the button. The visual change is done by setting the graphic in the cell to null and setting the text to the String, using TableCell.setText(String).
What I've realized - and worked around so far, is that TableCell.setText(String) doesn't change the data value associated with the cell in the TableView. It just changes the visual representation of the cell. The underlying data structure is in my case a ObservableList<String> that represents a row, and each element in the list is, of course, cell data.
My current solution is to set the underlying value doing this:
getTableView().getItems().get(getIndex()).set(getTableView().getColumns().indexOf(getTableColumn()), "Value");
And this works fine. But I mean, the code is barely readable.
It seems like the data in the TableView and the TableCell are entirely separated, since you need to access the TableView to set the underlying data for a cell. There is a TableCell.getItem() to get the data value, but there's no setItem(String) method to set it.
I hope I explained my issue good enough.
Is there a better and prettier way to do this? Why doesn't just `TableCell.setText(String) change the data value as well?
Edit: I'll explain what I am trying to implement:
I basically have a table where one column contains a button that will load some arbitrary data to the column when pressed. Once the data has been loaded, the button is removed from the column and the data is displayed instead. That is basically it. This works fine unless the table is sorted/filtered. Here's a MCVE of my implementation:
import javafx.animation.Animation;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.concurrent.Task;
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.stage.Stage;
import javafx.util.Callback;
import javafx.util.Duration;
public class MCVE extends Application {
private final BooleanProperty countLoading = new SimpleBooleanProperty(this, "countLoading", false);
#Override
public void start(Stage stage) {
int numOfCols = 3;
ObservableList<ObservableList<String>> tableData = FXCollections.observableArrayList();
// Generate dummy data.
for (int i = 0; i < 100; i++) {
ObservableList<String> row = FXCollections.observableArrayList();
for (int j = 0; j < numOfCols; j++)
row.add("Row" + i + "Col" + j);
tableData.add(row);
}
TableView<ObservableList<String>> table = new TableView<ObservableList<String>>();
// Add columns to the table.
for (int i = 0; i < numOfCols; i++) {
if (i == 2) {
final int j = i;
table.getColumns().add(addColumn(i, "Column " + i, e -> new QueueCountCell(j, countLoading)));
} else {
table.getColumns().add(addColumn(i, "Column " + i, null));
}
}
table.getItems().addAll(tableData);
Scene scene = new Scene(table);
stage.setScene(scene);
stage.show();
}
/**
* Returns a simple column.
*/
private TableColumn<ObservableList<String>, String> addColumn(int index, String name,
Callback<TableColumn<ObservableList<String>, String>, TableCell<ObservableList<String>, String>> callback) {
TableColumn<ObservableList<String>, String> col = new TableColumn<ObservableList<String>, String>(name);
col.setCellValueFactory(e -> new SimpleStringProperty(e.getValue().get(index)));
if (callback != null) {
col.setCellFactory(callback);
}
return col;
}
public static void main(String[] args) {
launch();
}
class QueueCountCell extends TableCell<ObservableList<String>, String> {
private final Button loadButton = new Button("Load");
public QueueCountCell(int colIndex, BooleanProperty countLoading) {
countLoading.addListener((obs, oldValue, newValue) -> {
if (newValue) {
loadButton.setDisable(true);
} else {
if (getIndex() >= 0 && getIndex() < this.getTableView().getItems().size()) {
loadButton.setDisable(false);
}
}
});
final Timeline timeline = new Timeline(new KeyFrame(Duration.ZERO, e -> setText("Loading .")),
new KeyFrame(Duration.millis(500), e -> setText("Loading . .")),
new KeyFrame(Duration.millis(1000), e -> setText("Loading . . .")),
new KeyFrame(Duration.millis(1500)));
timeline.setCycleCount(Animation.INDEFINITE);
loadButton.setOnAction(e -> {
new Thread(new Task<String>() {
#Override
public String call() throws InterruptedException {
// Simlute task working.
Thread.sleep(3000);
return "5";
}
#Override
public void running() {
setGraphic(null);
timeline.play();
countLoading.set(true);
}
#Override
public void succeeded() {
timeline.stop();
countLoading.set(false);
setText(getValue());
}
#Override
public void failed() {
timeline.stop();
countLoading.set(false);
setGraphic(loadButton);
setText(null);
this.getException().printStackTrace();
}
}).start();
});
}
#Override
public final void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if (item == null || empty) {
setGraphic(null);
} else {
setGraphic(loadButton);
}
}
}
}
Background: MVC
Much of JavaFX is designed around a Model-View-Controller (MVC) pattern. This is a loosely-defined pattern with many variants, but the basic idea is that there are three components:
Model: an object (or objects) that represent the data. The Model knows nothing about how the data is presented to the user.
View: an object that presents the data to the user. The view does not do any logical processing or store the data; it just knows how to convert the data to some kind of presentation for the user.
Controller: an object that modifies the data in the model, often (though not exclusively) in response to user input.
There are several variants of this pattern, including MVP, MVVM, supervising controller, passive view, and others, but the unifying theme in all of them is that there is a separation between the view, which simply presents data but does not otherwise "know" what the data is, and the model, which stores the state (data) but knows nothing about how it might be presented. The usually-cited motivation for this is the ability to have multiple views of the same data which have no need to refer to each other.
In the "classical" implementation of this, the view "observes" the model via some kind of subscriber-notification pattern (e.g. an observer pattern). So the view will register with the model to be notified of changes to the data, and will repaint accordingly. Often, since the controller relies on event listeners on the components in the view, the controller and view are tightly coupled; however there is always clear separation between the view and the model.
The best reference I know for learning more about this is Martin Fowler.
Background: JavaFX Virtualized Controls
JavaFX has a set of "virtualized controls", which includes ListView, TableView, TreeView, and TreeTableView. These controls are designed to be able to present large quantities of data to the user in an efficient manner. The key observation behind the design is that data is relatively inexpensive to store in memory, whereas the UI components (which typically have hundreds of properties) consume a relatively large amount of memory and are computationally expensive (e.g. to perform layout, apply style, etc). Moreover, in a table (for example) with a large amount of backing data, only a small proportion of those data are visible at any time, and there is no real need for UI controls for the remaining data.
Virtualized controls in JavaFX employ a cell rendering mechanism, in which "cells" are created only for the visible data. As the user scrolls around the table, the cells are reused to display data that was previously not visible. This allows the creation of a relatively small number of cells even for extremely large data sets: the number of (expensive) cells created is basically constant with respect to the size of the data. The Cell class defines an updateItem(...) method that is invoked when the cell is reused to present different data. All this is possible because the design is built on MVC principles: the cell is the view, and the data is stored in the model. The documentation for Cell has details on this.
Note that this means that you must not use the cell for any kind of data storage, because when the user scrolls in the control, that state will be lost. General MVC principles dictate that this is what you should do anyway.
The code you posted doesn't work correctly, as it violates these rules. In particular, if you click one of the "Load" buttons, and then scroll before the loading is complete, the cell that is performing the loading will now be referring to the wrong item in the model, and you end up with a corrupted view. The following series of screenshots occurred from pressing "Load", taking a screenshot, scrolling, waiting for the load to complete, and taking another screenshot. Note the value appears to have changed for an item that is different to the item for which "Load" was pressed.
To fix this, you have to have a model that stores all of the state of the application: you cannot store any state in the cells. It is a general truth in JavaFX that in order to make the UI code elegant, you should start with a well-defined data model. In particular, since your view (cell) changes when the data is in the process of loading, the "loading state" needs to be part of the model. So each item in each row in your table is represented by two pieces of data: the actual data value (strings in your case), and the "loading state" of the data.
So I would start with a class that represents that. You could just use a String for the data, or you could make it more general by making it a generic class. I'll do the latter. A good implementation will also keep the two states consistent: if the data is null and we have not explicitly stated it is loading, we consider it not loaded; if the data is non-null, we consider it loaded. So we have:
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.beans.property.SimpleObjectProperty;
public class LazyLoadingData<T> {
public enum LoadingState { NOT_LOADED, LOADING, LOADED }
private final ObjectProperty<T> data = new SimpleObjectProperty<>(null);
private final ReadOnlyObjectWrapper<LoadingState> loadingState
= new ReadOnlyObjectWrapper<>(LoadingState.NOT_LOADED);
public LazyLoadingData(T data) {
// listeners to keep properties consistent with each other:
this.data.addListener((obs, oldData, newData) -> {
if (newData == null) {
loadingState.set(LoadingState.NOT_LOADED);
} else {
loadingState.set(LoadingState.LOADED);
}
});
this.loadingState.addListener((obs, oldState, newState) -> {
if (newState != LoadingState.LOADED) {
this.data.set(null);
}
});
this.data.set(data);
}
public LazyLoadingData() {
this(null);
}
public void startLoading() {
loadingState.set(LoadingState.LOADING);
}
public final ObjectProperty<T> dataProperty() {
return this.data;
}
public final T getData() {
return this.dataProperty().get();
}
public final void setData(final T data) {
this.dataProperty().set(data);
}
public final ReadOnlyObjectProperty<LoadingState> loadingStateProperty() {
return this.loadingState.getReadOnlyProperty();
}
public final LazyLoadingData.LoadingState getLoadingState() {
return this.loadingStateProperty().get();
}
}
The model here will just be an ObservableList<List<LazyLoadingData<String>>>, so each cell is a LazyLoadingData<String> and each row is a list of them.
To make this properly MVC, let's have a separate controller class which has a way of updating data in the model:
import java.util.List;
import java.util.Random;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import javafx.concurrent.Task;
public class LazyLoadingDataController {
// data model:
private final List<List<LazyLoadingData<String>>> data ;
private final Random rng = new Random();
private final Executor exec = Executors.newCachedThreadPool(r -> {
Thread t = new Thread(r);
t.setDaemon(true);
return t ;
});
public LazyLoadingDataController(List<List<LazyLoadingData<String>>> data) {
this.data = data ;
}
public void loadData(int column, int row) {
Task<String> loader = new Task<String>() {
#Override
protected String call() throws InterruptedException {
int value = rng.nextInt(1000);
Thread.sleep(3000);
return "Data: "+value;
}
};
data.get(row).get(column).startLoading();
loader.setOnSucceeded(e -> data.get(row).get(column).setData(loader.getValue()));
exec.execute(loader);
}
}
Now our cell implementation is pretty straightforward. The only tricky part is that each item has two properties, and we actually need to observe both of those properties and update the cell if either of them changes. We need to be careful to remove listener from items the cell is no longer displaying. So the cell looks like:
import java.util.List;
import javafx.animation.Animation;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.beans.value.ChangeListener;
import javafx.scene.control.Button;
import javafx.scene.control.TableCell;
import javafx.util.Duration;
public class LazyLoadingDataCell<T>
extends TableCell<List<LazyLoadingData<T>>, LazyLoadingData<T>>{
private final Button loadButton = new Button("Load");
private final Timeline loadingAnimation = new Timeline(
new KeyFrame(Duration.ZERO, e -> setText("Loading")),
new KeyFrame(Duration.millis(500), e -> setText("Loading.")),
new KeyFrame(Duration.millis(1000), e -> setText("Loading..")),
new KeyFrame(Duration.millis(1500), e -> setText("Loading..."))
);
public LazyLoadingDataCell(LazyLoadingDataController controller, int columnIndex) {
loadingAnimation.setCycleCount(Animation.INDEFINITE);
loadButton.setOnAction(e -> controller.loadData(columnIndex, getIndex()));
// listener for observing either the dataProperty()
// or the loadingStateProperty() of the current item:
ChangeListener<Object> listener = (obs, oldState, newState) -> doUpdate();
// when the item changes, remove and add the listener:
itemProperty().addListener((obs, oldItem, newItem) -> {
if (oldItem != null) {
oldItem.dataProperty().removeListener(listener);
oldItem.loadingStateProperty().removeListener(listener);
}
if (newItem != null) {
newItem.dataProperty().addListener(listener);
newItem.loadingStateProperty().addListener(listener);
}
doUpdate();
});
}
#Override
protected void updateItem(LazyLoadingData<T> item, boolean empty) {
super.updateItem(item, empty);
doUpdate();
}
private void doUpdate() {
if (isEmpty() || getItem() == null) {
setText(null);
setGraphic(null);
} else {
LazyLoadingData.LoadingState state = getItem().getLoadingState();
if (state == LazyLoadingData.LoadingState.NOT_LOADED) {
loadingAnimation.stop();
setText(null);
setGraphic(loadButton);
} else if (state == LazyLoadingData.LoadingState.LOADING) {
setGraphic(null);
loadingAnimation.play();
} else if (state == LazyLoadingData.LoadingState.LOADED) {
loadingAnimation.stop();
setGraphic(null);
setText(getItem().getData().toString());
}
}
}
}
Note how
The cell contains no state. The fields in the cell are entirely related to the display of data (a button and an animation).
The action of the button doesn't (directly) change anything in the view. It simply tells the controller to update the data in the model. Because the cell (view) is observing the model, when the model changes, the view updates.
The model also changes independently of user action, when the task in the controller completes. Because the view is observing the model for changes, it updates automatically.
Finally an example using this. There is not much unexpected here, we just create a model (ObservableList of List<LazyLoadingData<String>>), create a controller, and then a table with some columns.
import java.util.List;
import javafx.application.Application;
import javafx.beans.property.SimpleObjectProperty;
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 LazyLoadingTableExample extends Application {
private final int numCols = 3 ;
private final int numRows = 100 ;
#Override
public void start(Stage primaryStage) {
TableView<List<LazyLoadingData<String>>> table = new TableView<>();
// data model:
ObservableList<List<LazyLoadingData<String>>> data
= FXCollections.observableArrayList();
table.setItems(data);
LazyLoadingDataController controller = new LazyLoadingDataController(data);
// build data:
for (int i = 0; i < numRows; i++) {
ObservableList<LazyLoadingData<String>> row
= FXCollections.observableArrayList();
for (int j = 0 ; j < numCols - 1 ; j++) {
row.add(new LazyLoadingData<>("Cell ["+j+", "+i+"]"));
}
row.add(new LazyLoadingData<>());
data.add(row);
}
for (int i = 0 ; i < numCols ; i++) {
table.getColumns().add(createColumn(controller, i));
}
Scene scene = new Scene(table, 600, 600);
primaryStage.setScene(scene);
primaryStage.show();
}
private TableColumn<List<LazyLoadingData<String>>,LazyLoadingData<String>>
createColumn(LazyLoadingDataController controller, int columnIndex) {
TableColumn<List<LazyLoadingData<String>>,LazyLoadingData<String>> col
= new TableColumn<>("Column "+columnIndex);
col.setCellValueFactory(cellData ->
new SimpleObjectProperty<>(cellData.getValue().get(columnIndex)));
col.setCellFactory(tc ->
new LazyLoadingDataCell<>(controller, columnIndex));
return col ;
}
public static void main(String[] args) {
launch(args);
}
}

JavaFX TreeView -- change background CSS on sibling nodes on MouseEvent

I am trying to implement onMouseEnter and onMouseExit events on a JavaFX ListView. What I want to do is if the mouse moves over a node of the list view, I want to change the background color of the nodes that are currently visible children in the current view.
This post has a great code sample, but is not quite what I am looking for.
Apply style to TreeView children nodes in javaFX
Using that code as a reference, what I am looking for is a given tree:
Root -> Item: 1 -> Item: 100 -> Item 1000, Item 1001, Item 1002, Item 1003
When I mouse over "Item: 100" I would like it and Item 1000* to have a background color change.
This seems difficult to me because the getNextSibling and getPreviousSibling interface is on the TreeItem and though you can get a TreeItem from a TreeCell on the MouseEvent, you can't (that I know of) update CSS on a TreeItem and have it take effect in a TreeCell -- because the setStyle method is on the TreeCell.
Suggestions on how this can be done?
[Update note: I originally had a solution using a subclass of TreeItem. The solution presented here is much cleaner than the original.]
Create an ObservableSet<TreeItem<?>> containing the TreeItems that should be highlighted. Then in the cell factory, observe that set, and the cell's treeItemProperty(), and set the style class (I used a PseudoClass in the example below) so the cell is highlighted if the tree item belonging to the cell is in the set.
Finally, register mouseEntered and mouseExited handlers with the cell. When the mouse enters the cell, you can get the tree item, use it to navigate to any other tree items you need, and add the appropriate items to the set you defined. In the mouseExited handler, clear the set (or perform other logic as needed).
import java.util.HashSet;
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.value.ChangeListener;
import javafx.collections.FXCollections;
import javafx.collections.ObservableSet;
import javafx.css.PseudoClass;
import javafx.scene.Scene;
import javafx.scene.control.TreeCell;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class HighlightingTree extends Application {
private final PseudoClass highlighted = PseudoClass.getPseudoClass("highlighted");
#Override
public void start(Stage primaryStage) {
TreeView<Integer> tree = new TreeView<>();
tree.setRoot(buildTreeRoot());
ObservableSet<TreeItem<Integer>> highlightedItems = FXCollections.observableSet(new HashSet<>());
tree.setCellFactory(tv -> {
// the cell:
TreeCell<Integer> cell = new TreeCell<Integer>() {
// indicates whether the cell should be highlighted:
private BooleanBinding highlightCell = Bindings.createBooleanBinding(() ->
getTreeItem() != null && highlightedItems.contains(getTreeItem()),
treeItemProperty(), highlightedItems);
// listener for the binding above
// note this has to be scoped to persist alongside the cell, as the binding
// will use weak listeners, and we need to avoid the listener getting gc'd:
private ChangeListener<Boolean> listener = (obs, wasHighlighted, isHighlighted) ->
pseudoClassStateChanged(highlighted, isHighlighted);
// anonymous constructor: register listener with binding
{
highlightCell.addListener(listener);
}
};
// display correct text:
cell.itemProperty().addListener((obs, oldItem, newItem) -> {
if (newItem == null) {
cell.setText(null);
} else {
cell.setText(newItem.toString());
}
});
// mouse listeners:
cell.setOnMouseEntered(e -> {
if (cell.getTreeItem() != null) {
highlightedItems.add(cell.getTreeItem());
highlightedItems.addAll(cell.getTreeItem().getChildren());
}
});
cell.setOnMouseExited(e -> highlightedItems.clear());
return cell ;
});
BorderPane uiRoot = new BorderPane(tree);
Scene scene = new Scene(uiRoot, 600, 600);
scene.getStylesheets().add("highlight-tree-children.css");
primaryStage.setScene(scene);
primaryStage.show();
}
private TreeItem<Integer> buildTreeRoot() {
return buildTreeItem(1);
}
private TreeItem<Integer> buildTreeItem(int n) {
TreeItem<Integer> item = new TreeItem<>(n);
if (n < 10_000) {
for (int i = 0; i<10; i++) {
item.getChildren().add(buildTreeItem(n * 10 + i));
}
}
return item ;
}
public static void main(String[] args) {
launch(args);
}
}
highlight-tree-children.css:
.tree-cell:highlighted {
-fx-background: yellow ;
}

Resources