fooforever
fooforever

Reputation: 389

How to create a JavaFX Tableview with editable rows of different types?

I'm trying to create a tableview with JvafaFX that will display a list of camera parameters. Some of these parameters are editable and some are not, some are constrained by a list of values others are free text entry. I have setup the table to display the parameters in a key, value type display where one column is used for the name of the parameter and the other the value.

The values for the table are set using an observable list which is generated from the background data model:

propertyNamesColumn.setCellValueFactory(cellData -> cellData.getValue().getName());
propertyValuesColumn.setCellValueFactory(cellData -> cellData.getValue().getValue());

The model also contains whether the property should be editable and what possible values it may contain, these are currently stored in two different fields (I'm not sure if this is the best way) so there are 4 fields in total.

When setting up the rows of the table I want to make those that should be editable (according to the values in the model), editable by either a choicebox containing the values from the possible values list or a text field.

However I'm not sure what I need to implement to make this happen, I have tried extending the ChoiceBoxTableCell class to add in this logic but the table cells never even become editable.

I'm fairly sure that either an extension of the choicebox cell type or a cell factory should be able to do this, but I don't know how.

Thanks for any help.

Upvotes: 1

Views: 1267

Answers (1)

James_D
James_D

Reputation: 209714

I think to do this, you need to create a generic model Parameter class, and use that as the type for your table. You can make it abstract and define an abstract getEditor() method (or perhaps delegate the editor factory to another class, but I will try to keep this as simple as possible). Then define subclasses which create different editors as required.

This might look something like this:

public abstract class Parameter<T> {

    private final BooleanProperty editable = new SimpleBooleanProperty();

    private final ObjectProperty<T> value = new SimpleObjectProperty<>();

    private final String name ;

    public Parameter(String name, T value, boolean editable) {
        this.name = name ;
        setValue(value);
        setEditable(editable);
    }

    public Parameter(String name, T value) {
        this(name, value, true);
    }

    public String getName() {
        return name ;
    }

    public ObjectProperty<T> valueProperty() {
        return value ;
    }

    public T getValue() {
        return valueProperty().get();
    }

    public void setValue(T value) {
        valueProperty().set(value);
    }

    public BooleanProperty editableProperty() {
        return editable ;
    }

    public boolean isEditable() {
        return editableProperty().get() ;
    }

    public void setEditable(boolean editable) {
        editableProperty().set(editable);
    }

    public abstract Node getEditor() ;

}

Then you might have a simple implementation like this for "free" strings:

public class StringParameter extends Parameter<String> {

    private final TextField editor ;

    public StringParameter(String name, String value) {
        super(name, value);
        editor = new TextField();
        editor.textProperty().bindBidirectional(valueProperty());
    }

    @Override
    public Node getEditor() { 
        return editor ;
    }

}

and maybe something like this for a spinner:

public class BoundIntegerParameter extends Parameter<Integer> {

    private final Spinner<Integer> editor ;

    public BoundIntegerParameter(int min, int max, String name, int value) {
        super(name, value);
        editor = new Spinner<>(min, max, value);
        editor.setEditable(true);
        editor.getValueFactory().valueProperty().bindBidirectional(valueProperty());
    }

    @Override
    public Node getEditor() {
        return editor ;
    }

}

For the "fixed list" you could similarly implement a FixedStringParameter which took a list of strings, and whose getEditor method returned a ComboBox. Another approach for fixed choices might be to use Enum types: this might look like

public class EnumParameter<E extends Enum<E>> extends Parameter<E> {

    private final ComboBox<E> editor ;

    public EnumParameter(String name, E value) {
        super(name, value);
        editor = new ComboBox<>();
        @SuppressWarnings("unchecked")
        Class<E> type = (Class<E>) value.getClass();
        E[] values = type.getEnumConstants() ;
        editor.getItems().setAll(values);
        editor.valueProperty().bindBidirectional(valueProperty());
    }

    @Override
    public Node getEditor() {
        return editor ;
    }

}

Now for the cell implementation for the value column, you need a bit of trickery. This seems to work:

public class ParameterValueEditingCell extends TableCell<Parameter<?>, Object> {


    @Override
    public void updateItem(Object item, boolean empty) {
        super.updateItem(item, empty);
        if (empty || item == null) {
            setText(null);
            setGraphic(null);
        } else {
            if (isEditing()) {
                setText(null);
                Parameter<?> param = getTableView().getItems().get(getIndex());
                setGraphic(param.getEditor());
            } else {
                setText(item.toString());
                setGraphic(null);
            }
        }
    }

    @Override
    public void startEdit() {
        // check if current parameter is editable and bail if not:

        int index = getIndex();
        if (index < 0 || index >= getTableView().getItems().size()) {
            return ;
        }
        if (! getTableView().getItems().get(index).isEditable()) {
            return ;
        }

        super.startEdit();
        setText(null);
        setGraphic(getTableView().getItems().get(getIndex()).getEditor());
    }

    @Override
    public void cancelEdit() {
        super.cancelEdit();
        Object item = getItem();
        setText(item == null ? null : item.toString());
        setGraphic(null);
    }

}

Finally, you can set things up as

TableView<Parameter<?>> table = new TableView<>();
table.setEditable(true);
TableColumn<Parameter<?>, Object> valueColumn = new TableColumn<>("Value");

// I can't see any way to set this up without the ugly (unchecked) cast
// Any ideas?
valueColumn.setCellValueFactory(cellData -> (ObservableValue<Object>)cellData.getValue().valueProperty());

valueColumn.setCellFactory(tc -> new ParameterValueEditingCell());

I made a complete example here

Upvotes: 4

Related Questions