I have a situation where I have a TableView. I'm trying to implement a feature that allows a cell to be moved up or down. After moving the cell up or down (with the cell content), I want to change focus to the new location of the cell.
The problem is that it doesn't change to the new location. It for some reason stays in the original selected cell location.
This is the code used to move up, move down and change focus:
I am attempting to move a single selected cell.
public class TableController
{
private ObservableList<SimpleStringProperty> observablePrnPropertyData;
#FXML
private TableView<SimpleStringProperty> table;
#FXML
private TableColumn<SimpleStringProperty, String> data;
#FXML
private void initialize()
{
this.data.setCellValueFactory(cellData -> cellData.getValue());
this.data.setCellFactory(event -> new EditCell(this.observablePrnPropertyData, this.table));
}
public void display(final PrnProperty prnProperty)
{
this.observablePrnPropertyData = PrnPropertyUtil.getObservableDataFromPrnProperty(prnProperty);
this.table.setItems(this.observablePrnPropertyData);
}
private final class EditCell extends TableCell<SimpleStringProperty, String>
{
#Override
public void updateItem(String item, boolean empty)
{
super.updateItem(item, empty);
if (empty)
{
this.setText(null);
this.setGraphic(null);
}
else
{
this.setUpContextMenu();
this.setText(this.getString());
this.setGraphic(null);
}
}
private void setUpContextMenu()
{
// Context menu
ContextMenu contextMenu = new ContextMenu();
// context menu Move up
this.moveUp = new MenuItem("Move up");
this.moveUp.setOnAction(event -> this.moveUp(this.table, this.observablePrnPropertyData));
contextMenu.getItems().add(this.moveUp);
// Context menu for move down
this.moveDown = new MenuItem("Move down");
this.moveDown.setOnAction(event -> this.moveDown(this.table, this.observablePrnPropertyData));
contextMenu.getItems().add(this.moveDown);
// Add context menu
this.setContextMenu(contextMenu);
}
public void moveUp(final TableView<?> table, ObservableList listToManipulate)
{
final int selectedIndex = table.getSelectionModel().getSelectedIndex();
final Object removeItem = listToManipulate.remove(selectedIndex);
final int newIndex = selectedIndex - 1;
listToManipulate.add(newIndex, removeItem);
this.changeTableCellFocus(table, newIndex);
}
public void moveDown(final TableView<?> table, ObservableList listToManipulate)
{
final int selectedIndex = table.getSelectionModel().getSelectedIndex();
final Object remove = listToManipulate.remove(selectedIndex);
final int newIndex = selectedIndex + 1;
listToManipulate.add(newIndex, remove);
this.changeTableCellFocus(table, newIndex);
}
public void changeTableCellFocus(final TableView<?> table, final int focusIndex)
{
table.requestFocus();
table.getSelectionModel().clearAndSelect(focusIndex);
table.getFocusModel().focus(focusIndex);
}
}
}
It would be great if someone can give a working example. I really want to know what i'm doing wrong.
If you just have to focus next and previous line, you can try: table.getFocusModel().focusNext() and .focusPrevious(), like described here: http://docs.oracle.com/javafx/2/api/javafx/scene/control/FocusModel.html
For your case use this code will work fine, it is simple you must first get the selected index, move the item to the next (or previous) row, then select this new row.
void upClicked(){
if(table.getSelectionModel().getSelectedItem() != null) // check if the user really selected a row in the table
{
if(table.getSelectionModel().getSelectedIndex() != 0) // if the row first one so do nothing
{
int index = table.getSelectionModel().getSelectedIndex(); // get the selected row index
SimpleStringProperty x = table.getSelectionModel().getSelectedItem(); // get the selected item
table.getItems().set(index, table.getItems().get(index-1)); // move the selected item up
table.getItems().set(index-1, x); // change the row with the item in above
table.getSelectionModel().select(index-1); // select the new row position
}
}
}
void downClicked(){
if(table.getSelectionModel().getSelectedItem() != null)
{
if(table.getSelectionModel().getSelectedIndex() != table.getItems().size()-1) // if the row is in last so dont do nothing
{
int index = table.getSelectionModel().getSelectedIndex();
SimpleStringProperty x = table.getSelectionModel().getSelectedItem();
table.getItems().set(index, table.getItems().get(index+1));
table.getItems().set(index+1, x);
table.getSelectionModel().select(index+1);
}
}
}
Related
In JavaFX's TableView (and TreeTableView) it's really hard to reorder columns using drag & drop when the horizontal scrollbar is present, because the table doesn't scroll automatically when one want's to drag the column to the currently not visible (off the scroll pane viewport) position.
I've noticed that there are already a bug (enhancement) reports for this:
https://bugs.openjdk.java.net/browse/JDK-8092314
https://bugs.openjdk.java.net/browse/JDK-8092355
https://bugs.openjdk.java.net/browse/JDK-8213739
... but as it haven't been tackled for quite some time I am wondering whether there is any other way to achieve the same behavior using the current API.
There is SSCCE:
public class TableViewColumnReorderDragSSCCE extends Application {
public static final int NUMBER_OF_COLUMNS = 30;
public static final int MAX_WINDOW_WIDTH = 480;
#Override
public void start(Stage stage) {
stage.setScene(new Scene(createTable()));
stage.show();
stage.setMaxWidth(MAX_WINDOW_WIDTH);
}
private TableView<List<String>> createTable() {
final TableView<List<String>> tableView = new TableView<>();
initColumns(tableView);
return tableView;
}
private void initColumns(TableView<List<String>> tableView) {
for (int i=0; i<NUMBER_OF_COLUMNS; i++) {
tableView.getColumns().add(new TableColumn<>("Column " + i));
}
tableView.getItems().add(Collections.emptyList());
}
}
Steps to reproduce:
Run the above SSCCE
Try to drag Column 0 after Column 29
I am after a fully functional solution (if any).
As no complete solution was provided I've came up with one of my own. I've introduced a (ColumnsOrderingEnhancer) implementation which will enhance the table view columns reordering by automatic scrolling (when needed).
Usage (with the table view defined in the above SSCCE):
// Enhance table view columns reordering
final ColumnsOrderingEnhancer<List<String>> columnsOrderingEnhancer = new ColumnsOrderingEnhancer<>(tableView);
columnsOrderingEnhancer.init();
ColumnsOrderingEnhancer implementation:
public class ColumnsOrderingEnhancer<T> {
private final TableView<T> tableView;
public ColumnsOrderingEnhancer(TableView<T> tableView) {
this.tableView = tableView;
}
public void init() {
tableView.skinProperty().addListener((observable, oldSkin, newSkin) -> {
// This can be done only when skin is ready
final TableHeaderRow header = (TableHeaderRow) tableView.lookup("TableHeaderRow");
final MouseDraggingDirectionHelper mouseDraggingDirectionHelper = new MouseDraggingDirectionHelper(header);
final ScrollBar horizontalScrollBar = getTableViewHorizontalScrollbar();
// This is the most important part which is responsible for scrolling table during the column dragging out of the viewport.
header.addEventFilter(MouseEvent.MOUSE_DRAGGED, event -> {
final double totalHeaderWidth = header.getWidth();
final double xMousePosition = event.getX();
final MouseDraggingDirectionHelper.Direction direction = mouseDraggingDirectionHelper.getLastDirection();
maybeChangeScrollBarPosition(horizontalScrollBar, totalHeaderWidth, xMousePosition, direction);
});
});
}
private void maybeChangeScrollBarPosition(ScrollBar horizontalScrollBar, double totalHeaderWidth, double xMousePosition, MouseDraggingDirectionHelper.Direction direction) {
if (xMousePosition > totalHeaderWidth && direction == RIGHT) {
horizontalScrollBar.increment();
}
else if (xMousePosition < 0 && direction == LEFT) {
horizontalScrollBar.decrement();
}
}
private ScrollBar getTableViewHorizontalScrollbar() {
Set<Node> scrollBars = tableView.lookupAll(".scroll-bar");
final Optional<Node> horizontalScrollBar =
scrollBars.stream().filter(node -> ((ScrollBar) node).getOrientation().equals(Orientation.HORIZONTAL)).findAny();
try {
return (ScrollBar) horizontalScrollBar.get();
}
catch (NoSuchElementException e) {
return null;
}
}
/**
* A simple class responsible for determining horizontal direction of the mouse during dragging phase.
*/
static class MouseDraggingDirectionHelper {
private double xLastMousePosition = -1;
private Direction direction = null;
MouseDraggingDirectionHelper(Node node) {
// Event filters that are determining when scrollbar needs to be incremented/decremented
node.addEventFilter(MouseEvent.MOUSE_PRESSED, event -> xLastMousePosition = event.getX());
node.addEventFilter(MouseEvent.MOUSE_DRAGGED, event -> {
direction = ((event.getX() - xLastMousePosition > 0) ? RIGHT : LEFT);
xLastMousePosition = event.getX();
});
}
enum Direction {
LEFT,
RIGHT
}
public Direction getLastDirection() {
return direction;
}
}
}
End result (which works surprisingly well):
It's not impossible to work around. You could start with something like this, though it is a very crude implementation, I'm sure in principle it can be refined to something reasonable:
tableView.setOnMouseExited(me -> {
if (me.isPrimaryButtonDown()) { // must be dragging
Bounds tvBounds = tableView.getBoundsInLocal();
double x = me.getX();
if (x < tvBounds.getMinX()) {
// Scroll to the left
tableView.scrollToColumnIndex(0);
} else if (x > tvBounds.getMaxX()) {
// Scroll to the right
tableView.scrollToColumnIndex(tableView.getColumns().size()-1);
}
}
});
In a proper implementation you would likely have to sneak around the Node hierarchy and find the width of the table columns and determine what the next out-of-view column is so you can scroll to the exact right column. Remember when you did that so you can do it again if the user continues to drag outside the table, but not too fast.
EDIT: Based on your self-answer, here is my take on it. I've compacted your code a bit and made it work on JavaFX 8.0:
static class TableHeaderScroller implements EventHandler<MouseEvent> {
private TableView tv;
private Pane header;
private ScrollBar scrollBar;
private double lastX;
public static void install(TableView tv) {
TableHeaderScroller ths = new TableHeaderScroller(tv);
tv.skinProperty().addListener(ths::skinListener);
}
private TableHeaderScroller(TableView tv) {
this.tv = tv;
}
private void skinListener(ObservableValue<? extends Skin<?>> observable, Skin<?> oldValue, Skin<?> newValue) {
if (header != null) {
header.removeEventFilter(MouseEvent.MOUSE_DRAGGED, this);
}
header = (Pane) tv.lookup("TableHeaderRow");
if (header != null) {
tv.lookupAll(".scroll-bar").stream().map(ScrollBar.class::cast)
.filter(sb -> sb.getOrientation() == Orientation.HORIZONTAL)
.findFirst().ifPresent( sb -> {
TableHeaderScroller.this.scrollBar = sb;
header.addEventFilter(MouseEvent.MOUSE_DRAGGED, TableHeaderScroller.this);
});
}
}
#Override
public void handle(MouseEvent event) {
double x = event.getX();
double sign = Math.signum(x - lastX);
lastX = x;
int dir = x < 0 ? -1 : x > header.getWidth() ? 1 : 0;
if (dir != 0 && dir == sign) {
if (dir < 0) {
scrollBar.decrement();
} else {
scrollBar.increment();
}
}
}
}
I want to have control over the styling of some rows of a TreeTableView based on the level in the tree. I used setRowFactory and apply a styling if this row is part of the first level children of the root of the Table. The styling works fine, but I also want to disable clicking on the checkbox for those rows. I am able to setDisable(true) but that also disables the expanding of the TreeItem and SetEditable(false) does not seem to have any effect.
EDIT: What I understand is that the Table must be set editable, then the columns are by default editable. But if I set TreeTableRow.setEditable(true); or TreeTableRow.setEditable(false); I never see any effect. The description seems of setEditable seems exactly what I want but I am unable to use it that way.
void javafx.scene.control.Cell.setEditable(boolean arg0)
setEditable
public final void setEditable(boolean value)
Allows for certain cells to not be able to be edited. This is useful incases >where, say, a List has 'header rows' - it does not make sense forthe header rows >to be editable, so they should have editable set tofalse.
Parameters:value - A boolean representing whether the cell is editable or not.If >true, the cell is editable, and if it is false, the cell can notbe edited.
Main:
public class TreeTableViewRowStyle extends Application {
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage stage) throws Exception {
// create the treeTableView and colums
TreeTableView<Person> ttv = new TreeTableView<Person>();
TreeTableColumn<Person, String> colName = new TreeTableColumn<>("Name");
TreeTableColumn<Person, Boolean> colSelected = new TreeTableColumn<>("Selected");
colName.setPrefWidth(100);
ttv.getColumns().add(colName);
ttv.getColumns().add(colSelected);
ttv.setShowRoot(false);
ttv.setEditable(true);
// set the columns
colName.setCellValueFactory(new TreeItemPropertyValueFactory<>("name"));
colSelected.setCellFactory(CheckBoxTreeTableCell.forTreeTableColumn(colSelected));
colSelected.setCellValueFactory(new TreeItemPropertyValueFactory<>("selected"));
ttv.setRowFactory(table-> {
return new TreeTableRow<Person>(){
#Override
public void updateItem(Person pers, boolean empty) {
super.updateItem(pers, empty);
boolean isTopLevel = table.getRoot().getChildren().contains(treeItemProperty().get());
if (!isEmpty()) {
if(isTopLevel){
setStyle("-fx-background-color:lightgrey;");
setEditable(false); //THIS DOES NOT SEEM TO WORK AS I WANT
//setDisable(true); //this would disable the checkbox but also the expanding of the tree
}else{
setStyle("-fx-background-color:white;");
}
}
}
};
});
// creating treeItems to populate the treetableview
TreeItem<Person> rootTreeItem = new TreeItem<Person>();
TreeItem<Person> parent1 = new TreeItem<Person>(new Person("Parent 1"));
TreeItem<Person> parent2 = new TreeItem<Person>(new Person("Parent 1"));
parent1.getChildren().add(new TreeItem<Person>(new Person("Child 1")));
parent2.getChildren().add(new TreeItem<Person>(new Person("Child 2")));
rootTreeItem.getChildren().addAll(parent1,parent2);
ttv.setRoot(rootTreeItem);
// build and show the window
Group root = new Group();
root.getChildren().add(ttv);
stage.setScene(new Scene(root, 300, 300));
stage.show();
}
}
Model Person :
public class Person {
private StringProperty name;
private BooleanProperty selected;
public Person(String name) {
this.name = new SimpleStringProperty(name);
selected = new SimpleBooleanProperty(false);
}
public StringProperty nameProperty() {
return name;
}
public BooleanProperty selectedProperty() {
return selected;
}
public void setName(String name){
this.name.set(name);
}
public void setSelected(boolean selected){
this.selected.set(selected);
}
}
The base problem is that none of the editable (nor the pseudo-editable like CheckBoxXX) Tree/Table cells respect the editability of the row they are contained in. Which I consider a bug.
To overcome, you have to extend the (pseudo) editable cells and make them respect the row's editable. The exact implementation is different for pseudo- vs. real editing cells. Below are in-line examples, for frequent usage you would make them top-level and re-use.
CheckBoxTreeTableCell: subclass and override updateItem to re-bind its disabled property like
colSelected.setCellFactory(c -> {
TreeTableCell cell = new CheckBoxTreeTableCell() {
#Override
public void updateItem(Object item, boolean empty) {
super.updateItem(item, empty);
if (getGraphic() != null) {
getGraphic().disableProperty().bind(Bindings
.not(
getTreeTableView().editableProperty()
.and(getTableColumn().editableProperty())
.and(editableProperty())
.and(getTreeTableRow().editableProperty())
));
}
}
};
return cell;
});
For a real editing cell, f.i. TextFieldTreeTableCell: override startEdit and return without calling super if the row isn't editable
colName.setCellFactory(c -> {
TreeTableCell cell = new TextFieldTreeTableCell() {
#Override
public void startEdit() {
if (getTreeTableRow() != null && !getTreeTableRow().isEditable()) return;
super.startEdit();
}
};
return cell;
});
Now you can toggle the row's editability as you do, changed the logic a bit to guarantee full cleanup in all cases:
ttv.setRowFactory(table-> {
return new TreeTableRow<Person>(){
#Override
public void updateItem(Person pers, boolean empty) {
super.updateItem(pers, empty);
// tbd: check for nulls!
boolean isTopLevel = table.getRoot().getChildren().contains(treeItemProperty().get());
if (!isEmpty() && isTopLevel) {
// if(isTopLevel){
setStyle("-fx-background-color:lightgrey;");
setEditable(false);
}else{
setEditable(true);
setStyle("-fx-background-color:white;");
}
}
};
});
Instead of creating a custom TreeTableCell subclass you can use the following utility method that basically installs a new cell-factory on a column that delegates to the original cell-factory but adds the row-editability binding whenever a cell is created.
public <S, T> void bindCellToRowEditability(TreeTableColumn<S, T> treeTableColumn) {
// Keep a handle on the original cell-factory.
Callback<TreeTableColumn<S, T>, TreeTableCell<S, T>> callback = treeTableColumn.getCellFactory();
// Install a new cell-factory that performs the delegation.
treeTableColumn.setCellFactory(column -> {
TreeTableCell<S, T> cell = callback.call(column);
// Add a listener so that we pick up when a new row is set for the cell.
cell.tableRowProperty().addListener((observable, oldRow, newRow) -> {
// If the new row is non-null, we proceed.
if (newRow != null) {
// We get the cell and row editable-properties.
BooleanProperty cellEditableProperty = cell.editableProperty();
BooleanProperty rowEditableProperty = newRow.editableProperty();
// Bind the cell's editable-property with its row's property.
cellEditableProperty.bind(rowEditableProperty);
}
});
return cell;
});
}
You can then set this for all columns of your TreeTableView as:
List<TreeTableColumn<S, ?>> columns = treeTableView.getColumns();
columns.forEach(this::bindCellToRowEditability);
You still need the custom TreeTableRow that checks whether it is top-level or not so that the editable value is correctly set for the row itself. However, setting the editable value on the row will now ensure that all cells in that row correctly reflects the row's editable-property.
If you want disable a specific Cell then handle the disable logic in the CellFactory rather than in RowFactory. The static method forTreeTableColumn(..) is a convinient method for quick use. But that is not the only way. You can still create your own factory for CheckBoxTreeTableCell.
So instead of
colSelected.setCellFactory(CheckBoxTreeTableCell.forTreeTableColumn(colSelected));
set the cellfactory as below, and this should work for you.
colSelected.setCellFactory(new Callback<TreeTableColumn<Person, Boolean>, TreeTableCell<Person, Boolean>>() {
#Override
public TreeTableCell<Person, Boolean> call(TreeTableColumn<Person, Boolean> column) {
return new CheckBoxTreeTableCell<Person, Boolean>() {
#Override
public void updateItem(Boolean item, boolean empty) {
super.updateItem(item, empty);
boolean isTopLevel = column.getTreeTableView().getRoot().getChildren().contains(getTreeTableRow().getTreeItem());
setEditable(!isTopLevel);
}
};
}
});
I have a table with many TableItems (Not a tableViewer), when I click on one of the table Items it get selected . The only way to deselect it is by selecting another TableItem. I want to implement a way to deselect The Table selection when The user click on the table Where there is no TableItems, or when ReSelecting the same TableItem.
table.addSelectionListener(new SelectionAdapter() {
#Override
public void widgetSelected(SelectionEvent e) {
if(e.item != ItemSelectioner ) {
ItemSelectioner = (TableItem)e.item;
// Blabla
}else {
ItemSelectioner = null;
table.deselectAll();
//blabla
}
}
});
As you can see, am using a selectionEvent which I think is the probleme, and using:
e.doit = false;
didn't work also.
Selection events are not generated for the empty parts of the table so you can't use a selection listener to do this.
You can use a mouse down listener and check if there is a table item at the mouse location:
table.addListener(SWT.MouseDown, event -> {
TableItem item = table.getItem(new Point(event.x, event.y));
if (item == null) { // No table item at the click location?
table.deselectAll();
}
});
To clear the selection the second time an item is clicked use something like this:
table.addListener(SWT.Selection, new Listener()
{
private int lastSelected = -1;
#Override
public void handleEvent(final Event event)
{
final int selectedIndex = table.getSelectionIndex();
if (selectedIndex < 0) {
lastSelected = -1;
return;
}
if (selectedIndex == lastSelected) {
table.deselect(selectedIndex);
lastSelected = -1;
}
else {
lastSelected = selectedIndex;
}
}
});
I have a table that lists objects of type Bot which have a name and isOn properties that I want to list:
private SimpleStringProperty name;
private boolean isOn;
The boolean isOn, I want to be read from a checkbox and also editable from that checkbox
So far, I have been able to add a checkbox to a column in my table for each row but it is purely visual (i.e. it is not tied to the Bot's isOn member).
How can I make the checkbox read and write from and to this member of Bot?
Here is my code dealing with the Table altogether:
ObservableList<Bot> bots = FXCollections.observableArrayList();
#FXML
private TableView<Bot> botTable;
#FXML
private TableColumn<Bot, String> nameColumn;
#FXML
private TableColumn<Bot, Boolean> statusColumn;
public void initialize(URL location, ResourceBundle resources){
nameColumn.setCellValueFactory(new PropertyValueFactory<Bot, String>("name"));
statusColumn.setCellValueFactory(new PropertyValueFactory<Bot, Boolean>("on"));
statusColumn.setSortable(false);
statusColumn.setCellValueFactory(new Callback<TableColumn.CellDataFeatures<Bot, Boolean>, ObservableValue<Boolean>>(){
#Override public ObservableValue<Boolean> call(TableColumn.CellDataFeatures<Bot, Boolean> features) {
return new SimpleBooleanProperty(features.getValue() != null);
}
});
// create a cell value factory with an add button for each row in the table.
statusColumn.setCellFactory(new Callback<TableColumn<Bot, Boolean>, TableCell<Bot, Boolean>>() {
#Override public TableCell<Bot, Boolean> call(TableColumn<Bot, Boolean> personBooleanTableColumn) {
return new AddBotCell(/*stage, botTable*/);
}
});
botTable.setItems(bots);
}
/** A table cell containing a button for adding a new person. */
private class AddBotCell extends TableCell<Bot, Boolean> {
// a checkbox for adding a new bot.
final CheckBox checkbox = new CheckBox();
// pads and centers the add button in the cell.
final StackPane paddedCheckBox = new StackPane();
AddBotCell(/*final Stage stage, final TableView table*/) {
paddedCheckBox.setPadding(new Insets(3));
paddedCheckBox.getChildren().add(checkbox);
checkbox.setOnMouseClicked(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
}
});
}
/** places an add checkbox in the row only if the row is not empty. */
#Override protected void updateItem(Boolean item, boolean empty) {
super.updateItem(item, empty);
if (!empty) {
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
setGraphic(checkbox);
}
}
}
You need to remove the checkbox, if the cell becomes empty. Furthermore you need to update the value when the user interacts with the CheckBox. This is better done from a listener to the selected property:
private class AddBotCell extends TableCell<Bot, Boolean> {
// a button for adding a new person.
final CheckBox checkbox = new CheckBox();
// pads and centers the add button in the cell.
final StackPane paddedCheckBox = new StackPane();
// records the y pos of the last button press so that the add person dialog can be shown next to the cell.
final DoubleProperty buttonY = new SimpleDoubleProperty();
private boolean updating = false;
AddBotCell(/*final Stage stage, final TableView table*/) {
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
paddedCheckBox.setPadding(new Insets(3));
paddedCheckBox.getChildren().add(checkbox);
checkbox.selectedProperty().addListener((o, oldValue, newValue) -> {
if (!updating) {
updating = true;
((Bot)getTableRow().getItem()).setIsOn(newValue);
updating = false;
}
});
}
/** places an add button in the row only if the row is not empty. */
#Override protected void updateItem(Boolean item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
setGraphic(null);
} else {
setGraphic(paddedCheckBox);
updating = true;
checkbox.setSelected(item);
updating = false;
}
}
}
Also your cellValueFactory should use the value of the property.
statusColumn.setCellValueFactory(new Callback<TableColumn.CellDataFeatures<Bot, Boolean>, ObservableValue<Boolean>>(){
#Override public ObservableValue<Boolean> call(TableColumn.CellDataFeatures<Bot, Boolean> features) {
return new SimpleBooleanProperty(features.getValue().isIsOn());
}
});
I am developing a JavaFX 2.2 application using Netbeans 7.2. I am working with a treeview and I extended TreeCell to provide to each TreeItem a context-menu with a MenuItem with "Collpase All" functionality. The max depth level of the treeview is 4. When a user right clicks on a TreeItem of level 2 and clicks to "Collapse All" MenuItem i want to make all the TreeItems of level 3 collapsed (setExpanded(false)). Below you can see the code that I am using. My problem is the memory and CPU cost of this operation. I inserted 250 TreeItems to level 3. The cost of a collapse all operation was ~200MB of memory on each collapseAll click and spends about 2s of time! My developer computer's CPU is an Intel i5 (3.3GHz) and I have 8GB of memory. Is this numbers of hardware cost normal or I am doing something wrong in my code? Am I using a wrong way to collapse them?
This class manages the TreeView. Loads data from database, reloads them, knows the selected TreeItem and expand/collapse the selected children TreeItems.
public final class TargetTree extends SqlConnectionManager {
private TreeView tree;
private TreeItem selectedItem;
private TargetTree() {
super();
this.tree = null;
this.selectedItem = null;
}
private TargetTree(TreeView tree) {
super();
this.tree = tree;
this.selectedItem = null;
}
public static TargetTree construct(TreeView tree) {
if (tree == null) {
return null;
}
TargetTree targetTree = new TargetTree(tree);
targetTree.load();
return targetTree;
}
public void reload() {
// Clear current tree.
if (tree.getRoot() != null) {
for (int i = 0; i < tree.getRoot().getChildren().size(); i++) {
tree.getRoot().getChildren().clear();
}
tree.setRoot(null);
}
this.load();
}
public void prune() {
//TODO
}
private void load() {
// New root Item.
final TreeItem<Object> treeRoot = new TreeItem<>((Object) "Root");
treeRoot.setExpanded(true);
// This integers help to find when to build a new department/section/measure.
int lastDepartmentId = -1;
int lastSectionId = -1;
int lastMeasureId = -1;
int lastTargetId = -1;
//The temp treeitems.
TreeItem<Object> departmentTreeItem = null;
TreeItem<Object> sectionTreeItem = null;
TreeItem<Object> measureTreeItem = null;
TreeItem<Object> targetTreeItem = null;
// Get the new TreeItems from the database.
super.errorMessage = "";
try {
// Establishing connection with db.
super.openConnection();
// Query to be executed. Selects everything from the database.
preparedStmt = connection.prepareStatement(
"SELECT.....ORDER BY....;");
resultSet = preparedStmt.executeQuery();
while (resultSet.next()) {
// Department Creation.
if (lastDepartmentId != resultSet.getInt("departmentId")) {
final Department department = Department.initEmpty();
department.setId(resultSet.getInt("departmentId"));
department.setName(resultSet.getString("departmentName"));
// Create the treeitem for this department.
departmentTreeItem = new TreeItem<>((Object) department);
departmentTreeItem.setExpanded(true);
treeRoot.getChildren().add(departmentTreeItem);
// Reset the children ids to ensure that they will be recreated.
lastDepartmentId = resultSet.getInt("departmentId");
lastSectionId = -1;
lastMeasureId = -1;
lastTargetId = -1;
}
// Section Creation.
if (lastSectionId != resultSet.getInt("sectionId")) {
final Section section = Section.initEmpty();
section.setId(resultSet.getInt("sectionId"));
section.setName(resultSet.getString("sectionName"));
// Create the treeitem for this section.
sectionTreeItem = new TreeItem<>((Object) section);
sectionTreeItem.setExpanded(true);
departmentTreeItem.getChildren().add(sectionTreeItem);
// Reset the children ids to ensure that they will be recreated.
lastSectionId = resultSet.getInt("sectionId");
lastMeasureId = -1;
lastTargetId = -1;
}
// Measure Creation.
if (lastMeasureId != resultSet.getInt("measureId")) {
final Measure measure = Measure.initEmpty();
measure.setId(resultSet.getInt("measureId"));
measure.setLastname(resultSet.getString("measureLastname"));
measure.setFirstname(resultSet.getString("measureFirstName"));
// Create the treeitem for this measure.
measureTreeItem = new TreeItem<>((Object) measure);
measureTreeItem.setExpanded(true);
sectionTreeItem.getChildren().add(measureTreeItem );
// Reset the children ids to ensure that they will be recreated.
lastMeasureId = resultSet.getInt("measureId");
lastTargetId = -1;
}
// Target Creation.
if (lastTargetId != resultSet.getInt("targetId")) {
final Target target = Target.initEmpty();
target.setId(resultSet.getInt("targetId"));
target.setText(resultSet.getString("targetText"));
// Create the treeitem for this target.
targetTreeItem = new TreeItem<>((Object) target);
targetTreeItem.setExpanded(false);
measureTreeItem.getChildren().add(targetTreeItem);
// Reset the children ids to ensure that they will be recreated.
lastTargetId = resultSet.getInt("targetId");
}
}
closeAll();
} catch (SQLException ex) {
super.errorMessage = ex.getMessage();
}
tree.setRoot(treeRoot);
final TargetTree targetTree = this;
tree.setCellFactory(new Callback<TreeView<Object>, TreeCell<Object>>() {
#Override
public TreeCell<Object> call(TreeView<Object> p) {
return new TargetTreeCell(targetTree);
}
});
// Select a Tree Item.
tree.getSelectionModel().selectedItemProperty().addListener(new ChangeListener() {
#Override
public void changed(ObservableValue observable, Object oldValue, Object newValue) {
selectedItem = (TreeItem) newValue;
}
});
}
public void collapseChildren() {
Thread thread = new Thread(new Task<Void>() {
#Override
protected Void call() throws Exception {
Platform.runLater(new Runnable() {
#Override
public void run() {
for (int i = 0; i < selectedItem.getChildren().size(); i++) {
TreeItem<Object> current = (TreeItem<Object>) selectedItem.getChildren().get(i);
if (!current.isLeaf()) {
current.setExpanded(false);
}
current = null;
}
selectedItem.setExpanded(false);
System.gc();
}
});
return null;
}
});
thread.setDaemon(true);
thread.start();
}
public void expandChildren() {
Thread thread = new Thread(new Task<Void>() {
#Override
protected Void call() throws Exception {
Platform.runLater(new Runnable() {
#Override
public void run() {
for (int i = 0; i < selectedItem.getChildren().size(); i++) {
TreeItem<Object> current = (TreeItem<Object>) selectedItem.getChildren().get(i);
if (!current.isLeaf()) {
current.setExpanded(true);
}
current = null;
}
selectedItem.setExpanded(true);
System.gc();
}
});
return null;
}
});
thread.setDaemon(true);
thread.start();
}
}
Below is the custom TreeCell class.
public class TargetTreeCell extends TreeCell<Object> {
private TargetTree targetTree;
public TargetTreeCell(TargetTree targetTree) {
super();
this.targetTree = targetTree;
}
#Override
public void updateItem(Object item, boolean empty) {
super.updateItem(item, empty);
if (item != null) {
if (item instanceof Target) {
initTarget(item);
} else if (item instanceof Measure) {
initMeasure(item);
} else if (item instanceof Section) {
initSection(item);
} else if (item instanceof Department) {
initDepartment(item);
} else if (item instanceof String) {
initRoot(item);
}
}
}
///<editor-fold defaultstate="collapsed" desc="Tree Item Initialization">
private void initRoot(Object item) {
// Create Menu Items.
MenuItem expandAllMenuItems = new MenuItem("Expand All");
MenuItem collapseAllMenuItems = new MenuItem("Collapse All");
// Event Haddlers for each Menu Items.
expandAllMenuItems.setOnAction(new EventHandler() {
#Override
public void handle(Event event) {
}
});
collapseAllMenuItems.setOnAction(new EventHandler() {
#Override
public void handle(Event event) {
targetTree.collapseChildren();
}
});
// Create Menu and add Menu Items.
ContextMenu contextMenu = new ContextMenu();
contextMenu.getItems().addAll(expandAllMenuItems, collapseAllMenuItems);
//Init Root Tree Item.
String root = (String) item;
setText(root);
setContextMenu(contextMenu);
}
private void initDepartment(Object item) {
// Create Menu Items.
MenuItem expandAllMenuItems = new MenuItem("Expand All");
MenuItem collapseAllMenuItems = new MenuItem("Collapse All");
// Event Haddlers for each Menu Items.
expandAllMenuItems.setOnAction(new EventHandler() {
#Override
public void handle(Event event) {
targetTree.expandChildren();
}
});
collapseAllMenuItems.setOnAction(new EventHandler() {
#Override
public void handle(Event event) {
targetTree.collapseChildren();
}
});
// Create Menu and add Menu Items.
ContextMenu contextMenu = new ContextMenu();
contextMenu.getItems().addAll(expandAllMenuItems, collapseAllMenuItems);
//Init Department Tree Item.
Department department = (Department) item;
setText(department.getName());
setContextMenu(contextMenu);
}
private void initSection(Object item) {
// Create Menu Items.
MenuItem expandAllMenuItems = new MenuItem("Expand All");
MenuItem collapseAllMenuItems = new MenuItem("Collapse All");
// Event Haddlers for each Menu Items.
expandAllMenuItems.setOnAction(new EventHandler() {
#Override
public void handle(Event event) {
targetTree.expandChildren();
}
});
collapseAllMenuItems.setOnAction(new EventHandler() {
#Override
public void handle(Event event) {
targetTree.collapseChildren();
}
});
// Create Menu and add Menu Items.
ContextMenu contextMenu = new ContextMenu();
contextMenu.getItems().addAll(expandAllMenuItems, collapseAllMenuItems);
//Init Section Tree Item.
Section section = (Section) item;
setText(section.getName());
setContextMenu(contextMenu);
}
private void initMeasure(Object item) {
// Create Menu Items.
MenuItem expandAllMenuItems = new MenuItem("Expand");
MenuItem collapseAllMenuItems = new MenuItem("Collapse");
// Event Haddlers for each Menu Items.
expandAllMenuItems.setOnAction(new EventHandler() {
#Override
public void handle(Event event) {
targetTree.expandChildren();
}
});
collapseAllMenuItems.setOnAction(new EventHandler() {
#Override
public void handle(Event event) {
targetTree.collapseChildren();
}
});
// Create Menu and add Menu Items.
ContextMenu contextMenu = new ContextMenu();
contextMenu.getItems().addAll(expandAllMenuItems, collapseAllMenuItems);
//Init Section Tree Item.
Measure measure = (Measure) item;
setText(measure.getLastname() + " " + measure.getFirstname());
setContextMenu(contextMenu);
}
private void initTarget(Object item) {
//Init Section Tree Item.
Target target = (Target) item;
setText(target.getText());
}
///</editor-fold>
}
If I have a copy-paste error please forgive me..I don't have problem with compiling. The code is running without errors. My problem is on the methods expandChildren() and collapseChildren() of the first class. In a previous version I didn't used threads and I used recursion to make all the children TreeItems (and their children TreeItems..) to collapse but the memory cost was more.
I found the answer to my problem! I will explain it with an example.
I initialize a TreeView with 100 TreeItems and the result is a tree structure with 3 levels.
On the screen the tree was displaying only 45 of them. To view the others i had to scroll up/down or to expand the collapsed TreeItems. On each case, the method updateItem is called to construct the new TreeItems that will appear to the visible on screen tree and therefore they all was appearing in the screen.
When i collapse an expanded TreeItem then the updateItem method will run. This was the reason of the memory and cpu cost! I had to collapse ~ 200 TreeItems that was all, and their parent expanded.
I solved my problem with a very simple way. Just before i started to collapse everything, i collapsed the parent TreeItem. Thus, i first collapsed the parent and then all the children. When the children was collapsed one by one from the source code (setExpanded(false)), the updateItem method was NOT running because their parent and therefore the children TreeItems was not existed in the screen.
On this way i saved a lot of memory and cpu time that i was spend like a dummy.
I did the same mistake,
happened as i implemented a MenuItem to collapse whole TreeItems (current selection as parent) child branches completely. But the collapse method clears the selection to minus one (-1) and that change wasn't visible because it didn't refreshed the parent item cell afterwards. So it seemed like nothing has changed at first sight because the focus was still visible on the same row.
I guess the skins selector needs to be cleared to do the collapse on child items, or taking over the selection index. so just collapse the parent item at first of which all child items should be folded and reset the selection index afterwards then unfold the parent item again.