Skip to content

Instantly share code, notes, and snippets.

@TomasMikula
Last active August 29, 2015 13:56
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save TomasMikula/6c5d97edc51ec8fa3d9e to your computer and use it in GitHub Desktop.
Save TomasMikula/6c5d97edc51ec8fa3d9e to your computer and use it in GitHub Desktop.
Using ReactFX to avoid multiple layouts.
package test.layoutproblem;
import static reactfx.EventStreams.*;
import java.util.ArrayList;
import java.util.List;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.event.EventHandler;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TreeCell;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyCombination;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.FlowPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import javafx.util.Callback;
import reactfx.EventSource;
import reactfx.EventStream;
import reactfx.Indicator;
public class LayoutProblem extends Application {
public static void main(String[] args) {
Application.launch(args);
}
@Override
public void start(Stage primaryStage) throws Exception {
BorderPane borderPane = new BorderPane();
TreeListPane treeListPane = new TreeListPane();
borderPane.setTop(new Button("A")); // here to give something other than the TreeView the focus
borderPane.setCenter(treeListPane);
MediaNode s1 = new MediaNode("Season 1", "S1", false, null);
MediaNode s2 = new MediaNode("Season 2", "S2", false, null);
s1.add(new MediaNode("Episode 1x01", "1x01", true, null));
s1.add(new MediaNode("Episode 1x02", "1x02", true, null));
s2.add(new MediaNode("Episode 2x01", "2x01", true, null));
s2.add(new MediaNode("Episode 2x02", "2x02", true, null));
/*
* Below, the treeListPane is configured. This involves setting up which nodes to display (mediaNodes)
* and whether or not the Tabs should be shown (expandTopLevel).
*
* Because changing either one of these properties must result in the (internal) TreeView's root to be
* changed, this is "expensive" (especially if the TreeItem's that are temporarily
* visible are the wrong ones and trigger background loads of images for example). Hence, the call
* to TreeListPane#buildTree should only occur ONCE, even when both mediaNodes and expandTopLevel are
* changed.
*
* This is accomplished internally by having an InvalidationListener on both these properties. The
* InvalidationListener calls requestLayout whenever buildTree was not called yet. In one of the
* compute* methods of TreeListPane this gets resolved and buildTree is called.
*
* Note: the tabs are not clickable (not a requirement), but you can use the cursor left/right to
* change them.
*/
treeListPane.beingUpdated.onWhile(() -> {
treeListPane.mediaNodes.addAll(s1, s2);
treeListPane.expandTopLevel.set(true);
treeListPane.focusedMediaNode.set(s1.children.get(1));
});
Scene scene = new Scene(borderPane);
scene.setFill(Color.BLACK);
scene.getStylesheets().add("test/layoutproblem/default.css");
primaryStage.setScene(scene);
primaryStage.show();
}
public static class TreeListPane extends BorderPane {
private static final KeyCombination LEFT = new KeyCodeCombination(KeyCode.LEFT);
private static final KeyCombination RIGHT = new KeyCodeCombination(KeyCode.RIGHT);
public final ObservableList<MediaNode> mediaNodes = FXCollections.observableArrayList();
public final ObjectProperty<MediaNode> focusedMediaNode = new SimpleObjectProperty<>();
public final BooleanProperty expandTopLevel = new SimpleBooleanProperty();
private final TreeView<MediaNode> treeView = new TreeView<>();
private final Filter filter = new Filter() {{
getStyleClass().add("seasons"); // FIXME this is way too specific
}};
private final InvalidationListener invalidateTreeListener = new InvalidationListener() {
@Override
public void invalidated(Observable observable) {
beingUpdated.onWhile(() -> treeInvalidations.push(null));
}
};
private final ChangeListener<TreeItem<MediaNode>> updateFocusedMediaNode = new ChangeListener<TreeItem<MediaNode>>() {
@Override
public void changed(ObservableValue<? extends TreeItem<MediaNode>> observable, TreeItem<MediaNode> old, TreeItem<MediaNode> current) {
focusedMediaNode.set(current == null ? null : current.getValue());
}
};
public final Indicator beingUpdated = new Indicator();
private final EventSource<Void> treeInvalidations = new EventSource<>();
private final EventStream<Void> buildTreeImpulse = emit(treeInvalidations).on(beingUpdated.offs());
public TreeListPane() {
focusedMediaNode.addListener(new ChangeListener<MediaNode>() {
@Override
public void changed(ObservableValue<? extends MediaNode> observable, MediaNode old, MediaNode current) {
setSelectedNode(current);
}
});
treeView.getStyleClass().add("main-list");
treeView.setEditable(false);
treeView.setShowRoot(false);
treeView.addEventHandler(KeyEvent.KEY_PRESSED, new EventHandler<KeyEvent>() {
@Override
public void handle(KeyEvent event) {
if(LEFT.match(event)) {
filter.activatePrevious();
treeView.getFocusModel().focus(0);
event.consume();
}
else if(RIGHT.match(event)) {
filter.activateNext();
treeView.getFocusModel().focus(0);
event.consume();
}
}
});
filter.activeProperty().addListener(new ChangeListener<Node>() {
@Override
public void changed(ObservableValue<? extends Node> observable, Node oldValue, Node value) {
Label oldLabel = (Label)oldValue;
Label label = (Label)value;
if(oldLabel != null) {
oldLabel.setText(((MediaNodeTreeItem)oldValue.getUserData()).getValue().getShortTitle());
}
label.setText(((MediaNodeTreeItem)value.getUserData()).getValue().getTitle());
refilter();
}
});
expandTopLevel.addListener(invalidateTreeListener);
mediaNodes.addListener(invalidateTreeListener);
buildTreeImpulse.subscribe(impulse -> buildTree());
setTop(filter);
setCenter(treeView);
}
private void buildTree() {
System.out.println(">>>>>>> buildTree");
treeView.getFocusModel().focusedItemProperty().removeListener(updateFocusedMediaNode); // prevent focus updates from changing tree root
treeView.setCellFactory(new Callback<TreeView<MediaNode>, TreeCell<MediaNode>>() {
@Override
public TreeCell<MediaNode> call(TreeView<MediaNode> param) {
return new MediaItemTreeCell();
}
});
filter.getChildren().clear();
if(expandTopLevel.get()) {
for(MediaNode node : mediaNodes) {
Label label = new Label(node.getShortTitle());
filter.getChildren().add(label);
label.setUserData(new MediaNodeTreeItem(node));
}
}
else {
treeView.setRoot(new MediaNodeTreeItem(new MediaNode("root", "root", false, mediaNodes), false));
}
treeView.getFocusModel().focusedItemProperty().addListener(updateFocusedMediaNode);
setSelectedNode(focusedMediaNode.get());
}
@Override
public void requestFocus() {
treeView.requestFocus();
}
private void refilter() {
MediaNodeTreeItem group = (MediaNodeTreeItem)filter.activeProperty().get().getUserData();
treeView.setRoot(group);
}
private final class MediaNodeTreeItem extends TreeItem<MediaNode> {
private final boolean isLeaf;
private boolean childrenPopulated;
MediaNodeTreeItem(MediaNode value, boolean isLeaf) {
super(value);
this.isLeaf = isLeaf;
}
MediaNodeTreeItem(MediaNode value) {
this(value, value.isLeaf());
}
@Override
public boolean isLeaf() {
return isLeaf;
}
@Override
public ObservableList<TreeItem<MediaNode>> getChildren() {
ObservableList<TreeItem<MediaNode>> treeChildren = super.getChildren();
if(!childrenPopulated) {
childrenPopulated = true;
for(MediaNode child : getValue().getChildren()) {
treeChildren.add(new MediaNodeTreeItem(child));
}
}
return treeChildren;
}
}
private final class MediaItemTreeCell extends TreeCell<MediaNode> {
private final DuoLineCell duoLineCell = new DuoLineCell();
@Override
protected void updateItem(final MediaNode mediaNode, boolean empty) {
super.updateItem(mediaNode, empty);
if(empty) {
setGraphic(null);
return;
}
duoLineCell.titleProperty().set(mediaNode.getTitle());
double maxWidth = treeView.getWidth() - 35;
duoLineCell.setMaxWidth(maxWidth); // WORKAROUND for being unable to restrict cells to the width of the view
duoLineCell.setPrefWidth(maxWidth);
setGraphic(duoLineCell);
}
}
private void setSelectedNode(MediaNode selectedMediaNode) {
TreeItem<MediaNode> focusedTreeItem = treeView.getFocusModel().getFocusedItem();
if(selectedMediaNode == null || (focusedTreeItem != null && selectedMediaNode.equals(focusedTreeItem.getValue()))) {
return;
}
List<MediaNode> stack = new ArrayList<>();
stack.add(selectedMediaNode);
while(stack.get(0).getParent() != null) {
stack.add(0, stack.get(0).getParent());
}
/*
* First level of the stack matches the filter (if used). This needs to be set
* correctly first to create a new root in the TreeView.
*/
if(!filter.getChildren().isEmpty()) {
for(Node n : filter.getChildren()) {
if(((MediaNodeTreeItem)n.getUserData()).getValue().equals(stack.get(0))) {
filter.activeProperty().set(n);
stack.remove(0);
break;
}
}
}
/*
* Get the TreeView's root, which either existed already or was just setup by
* adjusting the filter.
*/
TreeItem<MediaNode> treeItemForSelection = treeView.getRoot();
if(treeItemForSelection != null) {
/*
* Expand all TreeItem's towards the MediaNode to be selected.
*/
for(MediaNode node : stack) {
treeItemForSelection.setExpanded(true);
treeItemForSelection = findTreeItem(treeItemForSelection, node);
if(treeItemForSelection == null) {
break;
}
}
/*
* If after the search for the MediaNode to be selected we found a matching TreeItem
* that can be selected, it will be focused and scrolled to.
*/
if(treeItemForSelection != null) {
final int index = treeView.getRow(treeItemForSelection);
treeView.getFocusModel().focus(index);
Platform.runLater(new Runnable() {
@Override
public void run() {
treeView.scrollTo(index);
}
});
}
}
}
private static TreeItem<MediaNode> findTreeItem(TreeItem<MediaNode> root, MediaNode mediaNode) {
for(TreeItem<MediaNode> child : root.getChildren()) {
if(child.getValue().equals(mediaNode)) {
return child;
}
}
return null;
}
}
public static class Filter extends FlowPane {
private final ObjectProperty<Node> active = new SimpleObjectProperty<>();
public ObjectProperty<Node> activeProperty() { return active; }
public Filter() {
getStyleClass().add("filter");
getChildren().addListener(new ListChangeListener<Node>() {
@Override
public void onChanged(ListChangeListener.Change<? extends Node> event) {
update();
}
});
active.addListener(new ChangeListener<Node>() {
@Override
public void changed(ObservableValue<? extends Node> arg0, Node arg1, Node arg2) {
update();
}
});
}
public void activateNext() {
if(!getChildren().isEmpty()) {
int index = getChildren().indexOf(active.get());
index++;
if(index >= getChildren().size()) {
index = 0;
}
active.set(getChildren().get(index));
}
}
public void activatePrevious() {
if(!getChildren().isEmpty()) {
int index = getChildren().indexOf(active.get());
index--;
if(index < 0) {
index = getChildren().size() - 1;
}
active.set(getChildren().get(index));
}
}
private void update() {
Node activeNode = this.active.get();
for(Node node : getChildren()) {
node.setDisable(!node.equals(activeNode));
}
}
}
public static class MediaNode {
private final String title;
private final List<MediaNode> children;
private final String shortTitle;
private final boolean isLeaf;
private MediaNode parent;
public MediaNode(String title, String shortTitle, boolean isLeaf, List<MediaNode> children) {
this.title = title;
this.shortTitle = shortTitle;
this.isLeaf = isLeaf;
this.children = children == null ? new ArrayList<>() : new ArrayList<>(children);
}
public String getShortTitle() {
return shortTitle;
}
public List<MediaNode> getChildren() {
return children;
}
public boolean isLeaf() {
return isLeaf;
}
public MediaNode getParent() {
return parent;
}
public String getTitle() {
return title;
}
public void add(MediaNode child) {
if(child.parent != null) {
throw new IllegalStateException("cannot add child twice: " + child);
}
child.parent = this;
children.add(child);
}
}
public static class DuoLineCell extends HBox {
public StringProperty titleProperty() { return title.textProperty(); }
private final Label title = new Label() {{
getStyleClass().add("title");
}};
private final VBox description = new VBox() {{
getChildren().addAll(title);
}};
public DuoLineCell() {
setFillHeight(false);
setAlignment(Pos.CENTER_LEFT);
HBox.setHgrow(description, Priority.ALWAYS);
getChildren().addAll(description);
}
}
}
@TomasMikula
Copy link
Author

Posted in reply to openjfx-dev.
Related to RT-35830.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment