1

I am trying to write a JavaFx component which is generally called a "property editor" or a "property grid". A property being a name-value pair.

I guess the property sheet is made for this, but I'd like to use a TreeTableView. Mainly because I have nested properties and eventually several columns.

The component on the right is exactly what I try to achieve. treetableview

The problem I encountered with the TreeTableView, is the fact that the cell customisation must occur in the CellFactory which leads to a switch on the item type. This solution makes things really unflexible.

For example, what happens if a string value must be updated via a TextField for a given property and via a ComboBox for another property?

Any suggestion is more than welcome!

Relating questions: javafx-8-custom-listview-cells-its-evil


Update1

I tried to implement the @fabian's 1st suggestion.

I have my bean:

public class PropertyItem {

private StringProperty name = new SimpleStringProperty("");

private EditableItem value;
...
}

A default implementation of the EditableItem, to edit a string via in a TextField:

public class DefaultEditableItem implements EditableItem {

String value = "init value";
private TextField textField = new TextField();

public DefaultEditableItem(String value) {
    this.setValue(value);
}

// implementations of assignItem, removeItem, startEdit, cancelEdit,... as suggested for the cell behavior
}

My implementation of the TableView:

PropertyItem rootProp = new PropertyItem("ROOT", new    DefaultEditableItem("test roots"));
TreeItem<PropertyItem> root = new TreeItem(rootProp);

// the name column is straightforward ...

// value column
TreeTableColumn<PropertyItem, EditableItem> valueColumn = new TreeTableColumn<>("VALUE");
valueColumn.setCellValueFactory(new Callback<TreeTableColumn.CellDataFeatures<PropertyItem, EditableItem>, ObservableValue<EditableItem>>() {
    @Override
    public ObservableValue<EditableItem> call(TreeTableColumn.CellDataFeatures<PropertyItem, EditableItem> cellData) {
            TreeItem<PropertyItem> treeItem = cellData.getValue();
            PropertyItem propertyItem = treeItem.getValue();
            // this will not compile...
            return propertyItem.value();                
    }
});

valueColumn.setCellFactory(new Callback<TreeTableColumn<PropertyItem, EditableItem>, TreeTableCell<PropertyItem, EditableItem>>() {
    @Override
    public TreeTableCell<PropertyItem, EditableItem> call(TreeTableColumn<PropertyItem, EditableItem> param) {
            return new EditingTreeTableCell();
    }
});
valueColumn.setOnEditCommit(...)

treeTableView.getColumns().addAll(nameColumn, valueColumn);
treeTableView.setEditable(true);

My problem is on the cellValueFactory which needs to return a ObservableValue. What should I do, given that I want this column to be editable?

I guess that EditableItem must extends Property? But then, could my DefaultEditableItem extends SimpleStringProperty?

Community
  • 1
  • 1
Rascarcapac
  • 123
  • 8

1 Answers1

1

You could store information about how the item should be edited in the item itself (either directly or by allowing you to retrieve it from a map or similar data structure using a suitable key stored in the item).

Example:

public interface EditableItem {

    /**
     * Modify cell ui the way updateItem would do it, when the item is
     * added to the cell
     */
    void assignItem(EditingTreeTableCell<?, ?> cell);

    /**
     * Modify cell ui to remove the item the way it would be done in the updateItem method
     */
    void removeItem(EditingTreeTableCell<?, ?> cell);
}
public class EditingTreeTableCell<U, V> extends TreeTableCell<U, V> {

    @Override
    public void updateItem(V item, boolean empty) {
        boolean cleared = false;
        V oldItem = getItem();
        if (oldItem instanceof EditableItem) {
            ((EditableItem) oldItem).removeItem(this);
            cleared = true;
        }

        super.updateItem(item, empty);

        if (empty) {
            if (!cleared) {
                 setText("");
                 setGraphic(null);
            }
        } else {
             if (item instanceof EditableItem) {
                 ((EditableItem) item).assignItem(this);
             } else {
                 setText(Objects.toString(item, ""));
                 // or other default initialistation
             }
        }

    }

}

As this however would increase the size of the items, you could also store the info based on the type of the bean the property recides in and the name of the property, that is if the bean and the name property are assigned for the property:

public interface CellEditor<U, V> {

    /**
     * Modify cell ui the way updateItem would do it, when the item is
     * added to the cell
     */
    void assignItem(EditorTreeTableCell<U, V> cell, V item);

    /**
     * Modify cell ui to remove the item the way it would be done in the updateItem method
     */
    void removeItem(EditorTreeTableCell<U, V> cell);
}
public class EditorTreeTableCell<U, V> extends TreeTableCell<U, V> {

    public EditorTreeTableCell(Map<Class, Map<String, CellEditor<U, ?>>> editors) {
        this.editors = editors;
    }

    private CellEditor<U, V> editor;
    private final Map<Class, Map<String, CellEditor<U, ?>>> editors;

    @Override
    public void updateIndex(int i) {
        if (editor != null) {
            editor.removeItem(this);
            editor = null;
        }
        ObservableValue<V> observable = getTableColumn().getCellObservableValue(i);
        if (observable instanceof ReadOnlyProperty) {
            ReadOnlyProperty prop = (ReadOnlyProperty) observable;
            String name = prop.getName();
            Object bean = prop.getBean();
            if (name != null && bean != null) {
                 Class cl = bean.getClass();
                 while (editor == null  && cl != null) {
                     Map<String, CellEditor<U, ?>> map = editors.get(cl);
                     if (map != null) {
                          editor = (CellEditor) map.get(name);
                     }
                     cl = cl.getSuperclass();
                 }
            }
        }

        super.updateIndex(i);
    }

    public void updateItem(V item, boolean empty) {
        super.updateItem();
        if (editor == null) {
             setGraphic(null);
             setText(Objects.toString(item, ""));
        } else {
             editor.assignItem(this, item);
        }
    }

}

This would allow you to select the editor based on the object name and type of bean the object belongs to...

fabian
  • 80,457
  • 12
  • 86
  • 114
  • Thank you @fabian for the quick and detailed suggestions! I tried our first suggestion, but faced some difficulties. I am still missing some bricks! I updated my question with snippets. – Rascarcapac Nov 29 '16 at 01:05
  • @Rascarcapac In your question you'd need a `ObjectProperty`one way or another: Either you return `new SimpleObjectProperty(propertyItem.value())` or you modify the `value()` method to return a `ObjectProperty`. Alternatively you could use the second approach and initialize `name` as `name = new SimpleStringProperty(this, "name");` and use a map where `map.get(PropertyItem.class).get("name")` returns a suitable `CellEditor` – fabian Nov 29 '16 at 11:44