Last active
October 11, 2015 22:05
-
-
Save james-d/3da84819033a03db1496 to your computer and use it in GitHub Desktop.
Demo of creating TreeViews with dynamic connections between them. Not guaranteed to be memory-leak-free or efficient....
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package connectedtrees; | |
public class Assignment { | |
private final Task task ; | |
private final Employee assignee ; | |
public Assignment(Task task, Employee assignee) { | |
this.task = task; | |
this.assignee = assignee; | |
} | |
public Task getTask() { | |
return task; | |
} | |
public Employee getAssignee() { | |
return assignee; | |
} | |
@Override | |
public int hashCode() { | |
final int prime = 31; | |
int result = 1; | |
result = prime * result | |
+ ((assignee == null) ? 0 : assignee.hashCode()); | |
result = prime * result + ((task == null) ? 0 : task.hashCode()); | |
return result; | |
} | |
@Override | |
public boolean equals(Object obj) { | |
if (this == obj) | |
return true; | |
if (obj == null) | |
return false; | |
if (getClass() != obj.getClass()) | |
return false; | |
Assignment other = (Assignment) obj; | |
if (assignee == null) { | |
if (other.assignee != null) | |
return false; | |
} else if (!assignee.equals(other.assignee)) | |
return false; | |
if (task == null) { | |
if (other.task != null) | |
return false; | |
} else if (!task.equals(other.task)) | |
return false; | |
return true; | |
} | |
@Override | |
public String toString() { | |
return task + " -> " + assignee ; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package connectedtrees; | |
import javafx.beans.binding.Bindings; | |
import javafx.beans.property.ObjectProperty; | |
import javafx.beans.property.SimpleObjectProperty; | |
import javafx.geometry.Point2D; | |
import javafx.scene.Node; | |
import javafx.scene.shape.Line; | |
public class AssignmentView { | |
private final Assignment assignment ; | |
private final Line line = new Line(); | |
private final ObjectProperty<Point2D> start = new SimpleObjectProperty<>(this, "start"); | |
public final Point2D getStart() { | |
return start.get(); | |
} | |
public final void setStart(Point2D start) { | |
this.start.set(start); | |
} | |
public ObjectProperty<Point2D> startProperty() { | |
return start ; | |
} | |
private final ObjectProperty<Point2D> end = new SimpleObjectProperty<>(this, "end"); | |
public final Point2D getEnd() { | |
return end.get(); | |
} | |
public final void setEnd(Point2D end) { | |
this.end.set(end); | |
} | |
public ObjectProperty<Point2D> endProperty() { | |
return end ; | |
} | |
public AssignmentView(Assignment assignment) { | |
this.assignment = assignment ; | |
line.getStyleClass().add("assignment"); | |
line.setManaged(false); | |
line.startXProperty().bind(Bindings.createDoubleBinding(() -> { | |
if (start.get()==null) { | |
return 0.0 ; | |
} else { | |
return start.get().getX(); | |
} | |
}, start)); | |
line.startYProperty().bind(Bindings.createDoubleBinding(() -> { | |
if (start.get()==null) { | |
return 0.0 ; | |
} else { | |
return start.get().getY(); | |
} | |
}, start)); | |
line.endXProperty().bind(Bindings.createDoubleBinding(() -> { | |
if (end.get()==null) { | |
return 0.0 ; | |
} else { | |
return end.get().getX(); | |
} | |
}, end)); | |
line.endYProperty().bind(Bindings.createDoubleBinding(() -> { | |
if (end.get()==null) { | |
return 0.0 ; | |
} else { | |
return end.get().getY(); | |
} | |
}, end)); | |
line.visibleProperty().bind( | |
Bindings.isNotNull(start) | |
.and(Bindings.isNotNull(end))); | |
} | |
public Node getView(){ | |
return line ; | |
} | |
public Assignment getAssignment() { | |
return assignment ; | |
} | |
@Override | |
public int hashCode() { | |
final int prime = 31; | |
int result = 1; | |
result = prime * result | |
+ ((assignment == null) ? 0 : assignment.hashCode()); | |
return result; | |
} | |
@Override | |
public boolean equals(Object obj) { | |
if (this == obj) | |
return true; | |
if (obj == null) | |
return false; | |
if (getClass() != obj.getClass()) | |
return false; | |
AssignmentView other = (AssignmentView) obj; | |
if (assignment == null) { | |
if (other.assignment != null) | |
return false; | |
} else if (!assignment.equals(other.assignment)) | |
return false; | |
return true; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package connectedtrees; | |
import java.util.ArrayList; | |
import java.util.Arrays; | |
import java.util.List; | |
import java.util.stream.Collectors; | |
import javafx.application.Application; | |
import javafx.beans.binding.Bindings; | |
import javafx.beans.binding.ObjectBinding; | |
import javafx.beans.property.ObjectProperty; | |
import javafx.collections.FXCollections; | |
import javafx.collections.ObservableSet; | |
import javafx.collections.SetChangeListener.Change; | |
import javafx.geometry.Bounds; | |
import javafx.geometry.Point2D; | |
import javafx.geometry.Pos; | |
import javafx.scene.Node; | |
import javafx.scene.Scene; | |
import javafx.scene.control.Button; | |
import javafx.scene.control.ComboBox; | |
import javafx.scene.control.Label; | |
import javafx.scene.control.TreeCell; | |
import javafx.scene.control.TreeItem; | |
import javafx.scene.control.TreeView; | |
import javafx.scene.layout.BorderPane; | |
import javafx.scene.layout.HBox; | |
import javafx.scene.layout.Pane; | |
import javafx.stage.Stage; | |
public class ConnectedTrees extends Application { | |
private final List<Employee> employees = new ArrayList<>(); | |
private final List<Project> projects = new ArrayList<>(); | |
private final ObservableSet<Assignment> assignments = FXCollections.observableSet(); | |
private final ObservableSet<AssignmentView> assignmentViews = FXCollections.observableSet(); | |
@Override | |
public void start(Stage primaryStage) { | |
createData(); | |
TreeItem<Employee> employeeTreeRoot = buildEmployeeTree(); | |
TreeItem<Object> taskTreeRoot = buildTaskTree(); | |
TreeView<Object> taskTree = new TreeView<>(taskTreeRoot); | |
TreeView<Employee> employeeTree = new TreeView<>(employeeTreeRoot); | |
setUpTreeCellFactories(taskTree, employeeTree); | |
BorderPane root = new BorderPane(); | |
HBox controls = new HBox(5); | |
Label assignToLabel = new Label("Assign to:"); | |
ComboBox<Employee> assigneeCombo | |
= new ComboBox<Employee>(FXCollections.observableList(employees)); | |
Button assignButton = new Button("Assign"); | |
assignButton.disableProperty().bind( | |
Bindings.createBooleanBinding(() -> { | |
TreeItem<Object> selectedTask = taskTree.getSelectionModel().getSelectedItem(); | |
if (selectedTask == null) return true ; | |
if (! (selectedTask.getValue() instanceof Task)) return true ; | |
if (assigneeCombo.getValue() == null) return true; | |
return false ; | |
}, taskTree.getSelectionModel().selectedItemProperty(), assigneeCombo.valueProperty()) | |
); | |
assignButton.setOnAction(event -> | |
createAssignment((Task)taskTree.getSelectionModel().getSelectedItem().getValue(), | |
assigneeCombo.getValue())); | |
controls.getChildren().addAll(assignToLabel, assigneeCombo, assignButton); | |
controls.setAlignment(Pos.CENTER); | |
root.setTop(controls); | |
HBox trees = new HBox(15); | |
trees.getChildren().addAll(taskTree, employeeTree); | |
root.setCenter(trees); | |
setUpAssignmentListeners(trees); | |
Scene scene = new Scene(root,400,400); | |
scene.getStylesheets().add(getClass().getResource("application.css").toExternalForm()); | |
primaryStage.setScene(scene); | |
primaryStage.show(); | |
} | |
private void setUpTreeCellFactories(TreeView<Object> taskTree, TreeView<Employee> employeeTree) { | |
taskTree.setCellFactory(treeView -> { | |
TreeCell<Object> cell = new TreeCell<>(); | |
cell.itemProperty().addListener((obs, oldItem, newItem) -> { | |
if (oldItem != null && oldItem instanceof Task) { | |
assignmentViews.stream().filter(view -> view.getAssignment().getTask() == oldItem) | |
.forEach(view -> { | |
view.startProperty().unbind(); | |
view.setStart(null); | |
}); | |
} | |
if (newItem != null && newItem instanceof Task) { | |
assignmentViews.stream().filter(view -> view.getAssignment().getTask() == newItem) | |
.forEach(view -> bindLocation(view.startProperty(), cell, taskTree)); | |
} | |
cell.setText(newItem == null ? null : newItem.toString()); | |
}); | |
assignmentViews.addListener((Change<? extends AssignmentView> change) -> { | |
if (change.wasAdded() && change.getElementAdded().getAssignment().getTask() == cell.getItem()) { | |
AssignmentView view = change.getElementAdded(); | |
bindLocation(view.startProperty(), cell, taskTree); | |
} | |
}); | |
return cell ; | |
}); | |
employeeTree.setCellFactory(treeView -> { | |
TreeCell<Employee> cell = new TreeCell<Employee>() ; | |
cell.itemProperty().addListener((obs, oldEmployee, newEmployee) -> { | |
if (oldEmployee != null) { | |
assignmentViews.stream().filter(view -> view.getAssignment().getAssignee() == oldEmployee) | |
.forEach(view -> { | |
view.endProperty().unbind(); | |
view.setEnd(null); | |
}); | |
} | |
if (newEmployee != null) { | |
assignmentViews.stream().filter(view -> view.getAssignment().getAssignee() == newEmployee) | |
.forEach(view -> bindLocation(view.endProperty(), cell, employeeTree)); | |
} | |
cell.setText(newEmployee == null ? null : newEmployee.toString()); | |
}); | |
assignmentViews.addListener((Change<? extends AssignmentView> change) -> { | |
if (change.wasAdded() && change.getElementAdded().getAssignment().getAssignee() == cell.getItem()) { | |
AssignmentView view = change.getElementAdded(); | |
bindLocation(view.endProperty(), cell, taskTree); | |
} | |
}); | |
return cell ; | |
}); | |
} | |
private void bindLocation(ObjectProperty<Point2D> locationToBind, TreeCell<?> cell, TreeView<?> tree) { | |
Node treeParent = tree.getParent(); | |
ObjectBinding<Point2D> locationBinding = new ObjectBinding<Point2D>() { | |
{ | |
cell.localToSceneTransformProperty().addListener((obs, oldTransform, newTransform) -> this.invalidate()); | |
tree.boundsInParentProperty().addListener((obs, oldBounds, newBounds) -> this.invalidate()); | |
} | |
@Override | |
protected Point2D computeValue() { | |
Bounds cellBoundsInLocal = cell.getBoundsInLocal(); | |
Point2D cellCenter = new Point2D( | |
cellBoundsInLocal.getMinX() + cellBoundsInLocal.getWidth() / 2, | |
cellBoundsInLocal.getMinY() + cellBoundsInLocal.getHeight() / 2); | |
Point2D cellCenterInScene = cell.localToScene(cellCenter); | |
Point2D cellCenterInTreeParent = treeParent.sceneToLocal(cellCenterInScene); | |
return cellCenterInTreeParent ; } | |
}; | |
locationToBind.bind(locationBinding); | |
} | |
private void setUpAssignmentListeners(Pane container) { | |
assignments.addListener((Change<? extends Assignment> change) -> { | |
if (change.wasAdded()) { | |
Assignment newAssignment = change.getElementAdded(); | |
AssignmentView view = new AssignmentView(newAssignment); | |
assignmentViews.add(view); | |
} else if (change.wasRemoved()) { | |
Assignment removedAssignment = change.getElementRemoved(); | |
assignmentViews.remove(new AssignmentView(removedAssignment)); | |
} | |
}); | |
assignmentViews.addListener((Change<? extends AssignmentView> change) -> { | |
if (change.wasAdded()) { | |
AssignmentView view = change.getElementAdded(); | |
container.getChildren().add(view.getView()); | |
} else if (change.wasRemoved()) { | |
AssignmentView view = change.getElementRemoved(); | |
container.getChildren().remove(view.getView()); | |
view.startProperty().unbind(); | |
view.endProperty().unbind(); | |
} | |
}); | |
} | |
private void createData() { | |
employees.addAll(Arrays.asList( | |
new Employee("Bill Gates"), | |
new Employee("Tim Cook"), | |
new Employee("Larry Ellison"), | |
new Employee("Larry Page"), | |
new Employee("Mark Zuckerman") | |
)); | |
Project morningProject = new Project("Morning routine"); | |
morningProject.getTasks().addAll( | |
new Task("Make Coffee"), | |
new Task("Fetch news reports"), | |
new Task("Check daily calendar") | |
); | |
Project janitorialProject = new Project("Janitorial"); | |
janitorialProject.getTasks().addAll( | |
new Task("Clean desk"), | |
new Task("Sweep floor"), | |
new Task("Clean coffee maker") | |
); | |
Project adminProject = new Project("Administrative"); | |
adminProject.getTasks().addAll( | |
new Task("Organize filing"), | |
new Task("Order supplies") | |
); | |
projects.addAll(Arrays.asList( | |
morningProject, janitorialProject, adminProject | |
)); | |
} | |
private TreeItem<Employee> buildEmployeeTree() { | |
TreeItem<Employee> root = new TreeItem<>(new Employee("Staff")); | |
root.getChildren().addAll( | |
employees.stream().map( | |
employee -> new TreeItem<Employee>(employee) | |
).collect(Collectors.toList()) | |
); | |
return root ; | |
} | |
private TreeItem<Object> buildTaskTree() { | |
TreeItem<Object> root = new TreeItem<>("Projects"); | |
root.getChildren().addAll( | |
projects.stream().map( | |
project -> { | |
TreeItem<Object> item = new TreeItem<Object>(project); | |
item.getChildren().addAll( | |
project.getTasks().stream().map(TreeItem<Object>::new).collect(Collectors.toList()) | |
); | |
return item ; | |
} | |
).collect(Collectors.toList()) | |
); | |
return root ; | |
} | |
private void createAssignment(Task task, Employee employee) { | |
assignments.add(new Assignment(task, employee)); | |
} | |
public static void main(String[] args) { | |
launch(args); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package connectedtrees; | |
import javafx.beans.property.SimpleStringProperty; | |
import javafx.beans.property.StringProperty; | |
import javafx.collections.FXCollections; | |
import javafx.collections.ObservableList; | |
public class Employee { | |
private final StringProperty name = new SimpleStringProperty(this, "name"); | |
public final String getName() { | |
return name.get() ; | |
} | |
public final void setName(String name) { | |
this.name.set(name); | |
} | |
public StringProperty nameProperty() { | |
return name ; | |
} | |
public final ObservableList<Task> assignments = FXCollections.observableArrayList(); | |
public final ObservableList<Task> getAssignments() { | |
return assignments ; | |
} | |
public Employee(String name) { | |
setName(name); | |
} | |
@Override | |
public String toString() { | |
return name.get(); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package connectedtrees; | |
import javafx.beans.property.SimpleStringProperty; | |
import javafx.beans.property.StringProperty; | |
import javafx.collections.FXCollections; | |
import javafx.collections.ObservableList; | |
public class Project { | |
private final StringProperty title = new SimpleStringProperty(this, "title"); | |
public final String getTitle() { | |
return title.get(); | |
} | |
public final void setTitle(String title) { | |
this.title.set(title); | |
} | |
public StringProperty titleProperty() { | |
return title ; | |
} | |
private final ObservableList<Task> tasks = FXCollections.observableArrayList(); | |
public final ObservableList<Task> getTasks() { | |
return tasks ; | |
} | |
public Project(String title) { | |
setTitle(title); | |
} | |
@Override | |
public String toString() { | |
return title.get(); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package connectedtrees; | |
import javafx.beans.property.SimpleStringProperty; | |
import javafx.beans.property.StringProperty; | |
public class Task { | |
private final StringProperty title = new SimpleStringProperty(this, "title"); | |
public final String getTitle() { | |
return title.get() ; | |
} | |
public final void setTitle(String title) { | |
this.title.set(title); | |
} | |
public StringProperty titleProperty() { | |
return title ; | |
} | |
public Task(String title) { | |
setTitle(title); | |
} | |
@Override | |
public String toString() { | |
return title.get(); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment