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.
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?
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...
Related
I have a particular TreeTableView that displays a hierarchical tree of mixed types. These types do not necessarily have overlapping columns and as such the columns for some rows will be empty. As an example, consider the following classes:
public class Person {
private final StringProperty nameProperty;
private final StringProperty surnameProperty;
public Person() {
this.nameProperty = new SimpleStringProperty();
this.surnameProperty = new SimpleStringProperty();
}
public StringProperty nameProperty() {
return this.nameProperty;
}
public void setName(String value) {
this.nameProperty.set(value);
}
public String getName() {
return this.nameProperty.get();
}
public StringProperty surnameProperty() {
return this.surnameProperty;
}
public void setSurname(String value) {
this.surnameProperty.set(value);
}
public String getSurname() {
return this.surnameProperty.get();
}
}
public class Dog {
private final StringProperty nameProperty;
private final IntegerProperty ageProperty;
private final StringProperty breedProperty;
public Dog() {
this.nameProperty = new SimpleStringProperty();
this.ageProperty = new SimpleIntegerProperty();
this.breedProperty = new SimpleStringProperty();
}
public StringProperty nameProperty() {
return this.nameProperty;
}
public void setName(String value) {
this.nameProperty.set(value);
}
public String getName() {
return this.nameProperty.get();
}
public IntegerProperty ageProperty() {
return this.ageProperty;
}
public void setAge(int value) {
this.ageProperty.setValue(value);
}
public int getAge() {
return this.ageProperty.get();
}
public StringProperty breedProperty() {
return this.breedProperty;
}
public void setBreed(String breed) {
this.breedProperty.set(breed);
}
public String getBreed() {
return this.breedProperty.get();
}
}
If I construct the TreeTableView as follows:
TreeTableView<Object> treeTableView = new TreeTableView<>();
treeTableView.setEditable(true);
List<TreeTableColumn<Object, ?>> columns = treeTableView.getColumns();
TreeTableColumn<Object, String> nameColumn = new TreeTableColumn<>("Name");
nameColumn.setCellValueFactory(new TreeItemPropertyValueFactory<>("name"));
nameColumn.setCellFactory(TextFieldTreeTableCell.forTreeTableColumn());
columns.add(nameColumn);
TreeTableColumn<Object, String> surnameColumn = new TreeTableColumn<>("Surname");
surnameColumn.setCellFactory(TextFieldTreeTableCell.forTreeTableColumn());
surnameColumn.setCellValueFactory(new TreeItemPropertyValueFactory<>("surname"));
columns.add(surnameColumn);
TreeTableColumn<Object, Integer> ageColumn = new TreeTableColumn<>("Age");
ageColumn.setCellFactory(TextFieldTreeTableCell.forTreeTableColumn(new IntegerStringConverter()));
ageColumn.setCellValueFactory(new TreeItemPropertyValueFactory<>("age"));
columns.add(ageColumn);
TreeTableColumn<Object, String> breedColumn = new TreeTableColumn<>("Breed");
breedColumn.setCellFactory(TextFieldTreeTableCell.forTreeTableColumn());
breedColumn.setCellValueFactory(new TreeItemPropertyValueFactory<>("breed"));
columns.add(breedColumn);
TreeItem<Object> rootItem = new TreeItem<>();
treeTableView.setRoot(rootItem);
treeTableView.setShowRoot(false);
List<TreeItem<Object>> rootChildren = rootItem.getChildren();
Person john = new Person();
john.setName("John");
john.setSurname("Denver");
TreeItem<Object> johnTreeItem = new TreeItem<>(john);
rootChildren.add(johnTreeItem);
List<TreeItem<Object>> johnChildren = johnTreeItem.getChildren();
Dog charlie = new Dog();
charlie.setName("Charlie");
charlie.setAge(4);
charlie.setBreed("Labrador");
TreeItem<Object> charlieTreeItem = new TreeItem<>(charlie);
johnChildren.add(charlieTreeItem);
Dog daisy = new Dog();
daisy.setName("Daisy");
daisy.setAge(7);
daisy.setBreed("Bulldog");
TreeItem<Object> daisyTreeItem = new TreeItem<>(daisy);
johnChildren.add(daisyTreeItem);
I will get a TreeTableView that looks like:
The Age and Breed columns are empty for the TreeItems that contains Person objects. However, nothing stops me from editing Age or Breed cell for the top-most Person row. Setting a value in one of those cells doesn't change the Person object, but the value still hangs around there like it is committed.
Is there any way to prevent this from happening? I know that I could check for nulls in a custom TreeTableCell subclass and prevent the editing from kicking off in the startEdit() method. However, there are circumstances where a null-value is valid and preventing editing by checking nulls is not a feasible solution for all situations. Also, creating a custom TreeTableCell subclass for every datatype and corresponding columns is painful. It would have been nice if TreeItemPropertyValueFactory could provide for a way to abort the edit when no value is present for a particular cell.
Ok, I scraped together something by looking at the TreeItemPropertyValueFactory class itself for inspiration. This gives me the desired functionality, although I'm not sure if it is 100% correct or what the implications are of using it.
It basically comes down to installing a new cell-factory that checks if the cell-value-factory is of type TreeItemPropertyValueFactory. If it is the case, a new cell-factory is installed that delegates to the original but adds listeners for the table-row and tree-item properties. When the TreeItem changes, we get the row-data and see if we can access the desired property (via a PropertyReference that is cached for performance). If we can't (and we get the two exceptions) we assume that the property cannot be accessed and we set the cell's editable-property to false.
public <S, T> void disableUnavailableCells(TreeTableColumn<S, T> treeTableColumn) {
Callback<TreeTableColumn<S, T>, TreeTableCell<S, T>> cellFactory = treeTableColumn.getCellFactory();
Callback<CellDataFeatures<S, T>, ObservableValue<T>> cellValueFactory = treeTableColumn.getCellValueFactory();
if (cellValueFactory instanceof TreeItemPropertyValueFactory) {
TreeItemPropertyValueFactory<S, T> valueFactory = (TreeItemPropertyValueFactory<S, T>)cellValueFactory;
String property = valueFactory.getProperty();
Map<Class<?>, PropertyReference<T>> propertyRefCache = new HashMap<>();
treeTableColumn.setCellFactory(column -> {
TreeTableCell<S, T> cell = cellFactory.call(column);
cell.tableRowProperty().addListener((o1, oldRow, newRow) -> {
if (newRow != null) {
newRow.treeItemProperty().addListener((o2, oldTreeItem, newTreeItem) -> {
if (newTreeItem != null) {
S rowData = newTreeItem.getValue();
if (rowData != null) {
Class<?> rowType = rowData.getClass();
PropertyReference<T> reference = propertyRefCache.get(rowType);
if (reference == null) {
reference = new PropertyReference<>(rowType, property);
propertyRefCache.put(rowType, reference);
}
try {
reference.getProperty(rowData);
} catch (IllegalStateException e1) {
try {
reference.get(rowData);
} catch (IllegalStateException e2) {
cell.setEditable(false);
}
}
}
}
});
}
});
return cell;
});
}
}
For the example listed in the question, you can call it after you created all your columns as:
...
columns.forEach(this::disableUnavailableCells);
TreeItem<Object> rootItem = new TreeItem<>();
treeTableView.setRoot(rootItem);
treeTableView.setShowRoot(false);
...
You'll see that cells for the Age and Breed columns are now uneditable for Person entries whereas cells for the Surname column is now uneditable for Dog entries, which is what we want. Cells for the common Name column is editable for all entries as this is a common property among Person and Dog objects.
In the JavaFx ComboBox which uses a class object list .I want to select items in the ComboBox programmatically using getSelectionModel().select(object or index). i am not getting the desired result Although the value is set but it is something like this main.dao.Company.Company.CompanyTableData#74541e7b.
The code is somewhat like this.
ComboBox<CompanyTableData> company = new ComboBox<>();
company.setItems(GetCompany.getCompanyTableData());//where Observable list is set..
GetCompany.getCompanyTableData() returns observablelist of CompanyTableData class.
The ComboBox Looks as follows.
The CompanyTableData Class is as.
public class CompanyTableData {
private SimpleStringProperty itemCompanyId;
private SimpleStringProperty itemCompanyName;
private SimpleStringProperty createBy;
private SimpleStringProperty createdOn;
public CompanyTableData(CompanyData companyData){
this.itemCompanyId = new SimpleStringProperty(companyData.getItemCompanyId());
this.itemCompanyName = new SimpleStringProperty(companyData.getItemCompanyName());
this.createBy = new SimpleStringProperty(companyData.getCreatedBy());
this.createdOn = new SimpleStringProperty(companyData.getCreatedOn());
}
public String getItemCompanyId() {
return itemCompanyId.get();
}
public SimpleStringProperty itemCompanyIdProperty() {
return itemCompanyId;
}
public void setItemCompanyId(String itemCompanyId) {
this.itemCompanyId.set(itemCompanyId);
}
public String getItemCompanyName() {
return itemCompanyName.get();
}
public SimpleStringProperty itemCompanyNameProperty() {
return itemCompanyName;
}
public void setItemCompanyName(String itemCompanyName) {
this.itemCompanyName.set(itemCompanyName);
}
public String getCreateBy() {
return createBy.get();
}
public SimpleStringProperty createByProperty() {
return createBy;
}
public void setCreateBy(String createBy) {
this.createBy.set(createBy);
}
public String getCreatedOn() {
return createdOn.get();
}
public SimpleStringProperty createdOnProperty() {
return createdOn;
}
public void setCreatedOn(String createdOn) {
this.createdOn.set(createdOn);
}
}
The Cell Factory is set
company.setCellFactory(param -> new CompanyCell());
And the CompanyCell
public class CompanyCell extends ListCell<CompanyTableData> {
#Override
protected void updateItem(CompanyTableData item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null || item.getItemCompanyName() == null) {
setText(null);
} else {
setText(item.getItemCompanyName());
}
}
}
After all this when i try to set the items programmetically as
company.getSelectionModel().select(getSelectedCompanyIndex());
The getSelectedCompanyIndex() function is as follows.
public static CompanyTableData getSelectedCompanyIndex(){
CompanyTableData c = null,i;
Iterator<CompanyTableData> itr = GetCompany.getCompanyTableData().iterator();
while (itr.hasNext()){
i = itr.next();
if (i.getItemCompanyName().equals(Element.getItemTableData().getCompany())){
c = i;
}
}
return c;
}
And the result i am getting is
And
At the end it should select a name or item in the list but it has set some type of object i think.
Now what should i do. Is there any type of string conversion required.
The buttonCell used to display the item when the combobox popup is not shown is not automatically created using the cellFactory. You need to set this property too to use the same cell implementation:
company.setCellFactory(param -> new CompanyCell());
company.setButtonCell(new CompanyCell());
i have built GUI with combo box. I have ObservableList<SimpleTableObject> types
which should display types of material. It looks like this
material_comboBox_type.getItems().addAll(types);
material_comboBox_type.setCellFactory((ListView<SimpleTableObject>
param) -> {
final ListCell<SimpleTableObject> cell = new
ListCell<SimpleTableObject>() {
#Override
public void updateItem(SimpleTableObject item, boolean empty) {
super.updateItem(item, empty);
if (item != null) {
setText(item.getName().get());//return String, actuall name of material
}
else {
setText(null);
}
}
};
return cell;
});
Now the problem is this: when i click combobox, it shows names as desired. But when i select one, instead of the string property, an object itself is displayed, which looks like that classes.SimpleTableObject#137ff5c.
How can I achieve it?
The selected item in a combo box is displayed in a cell called the buttonCell. So you need to set the button cell as well as the cell factory (which generates the cells in the dropdown).
To do this, it's probably easier to refactor your cell implementation as a (named) inner class:
private static class SimpleTableObjectListCell extends ListCell<SimpleTableObject> {
#Override
public void updateItem(SimpleTableObject item, boolean empty) {
super.updateItem(item, empty);
if (item != null) {
setText(item.getName().get());//return String, actuall name of material
}
else {
setText(null);
}
}
}
And then:
materialComboBoxType.setCellFactory(listView -> new SimpleTableObjectListCell());
materialComboBoxType.setButtonCell(new SimpleTableObjectListCell());
Ok, i did this with converter:
material_comboBox_type.setConverter(new StringConverter<SimpleTableObject>() {
#Override
public String toString(SimpleTableObject object) {
return object.getName().get();
}
#Override
public SimpleTableObject fromString(String string) {
throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
}
});
If I have an object that has an arrayList of objects:
class Event{
private ArrayList<Room> rooms;
//..
public void setRooms(ArrayList<Room> rooms) {
this.rooms = rooms;
}
public ArrayList<Room> getRooms() {
return rooms;
}
}
//---------------------
class Room{
private String roomId;
private String roomName;
public Room(String roomId, String roomName) {
this.roomId = roomId;
this.roomName = roomName;
}
public String getRoomId() {
return roomId;
}
public String getRoomName() {
return roomName;
}
public void setRoomId(String roomId) {
this.roomId = roomId;
}
public void setRoomName(String roomName) {
this.roomName = roomName;
}
}
How can I create a combobox in my table from the array of room objects?
What I have that is only showing an object identifier of some sort.
TableColumn<Event, ArrayList> roomsColumn = new TableColumn<>("Room Select");
roomsColumn.setMinWidth(200);
roomsColumn.setCellValueFactory(new PropertyValueFactory<>("rooms"));
//Create an observable list to populate the table with.
ObservableList<Event> eventList = FXCollections.observableArrayList();
//loop the json to populate the observable list
for (Event event : events.getEventList() ){
eventList.add(event);
}
//populate the table
eventTable.setItems(eventList);
eventTable.getColumns().addAll(eventColumn, bDateColumn, eDateColumn, roomsColumn);
**All of the columns are built but the rooms column shows a comma separated list of room objects:
com.***.Room#345, com.***.Room#653, com.***.Room#889
You need a custom cell factory to return a TableCell with a ComboBox.
roomsColumn.setCellFactory(call -> {
// create a new cell for array lists
return new TableCell<Event, ArrayList<String>>() {
#Override
protected void updateItem(ArrayList<String> item, boolean empty) {
super.updateItem(item, empty);
// if there is no item, return an empty cell
if (empty || item == null) {
setGraphic(null);
}
else {
ComboBox<String> box = new ComboBox<>();
// set combo box items
box.setItems(FXCollections.observableArrayList(item));
// listen for changes
box.valueProperty().addListener((observable, oldValue, newValue) -> {
System.out.println("new room "+newValue);
});
// set cell contents
setGraphic(box);
}
}
};
});
This answer provides a solution for an observable list that will send "list updated" notifications if properties of elements of the list change.
In my case, elements (a Element class) of such observable list are complex and I don't like to implement property for each member variable. Due to this, I added into the Element class a BooleanProperty that indicates change of the class.
Element Class
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.ReadOnlyBooleanWrapper;
public class Element {
// ...
private ReadOnlyBooleanWrapper changeIndicatorWrapper;
public Element() {
//...
changeIndicatorWrapper = new ReadOnlyBooleanWrapper(false);
}
public ReadOnlyBooleanProperty changeIndicatorProperty() {
return changeIndicatorWrapper.getReadOnlyProperty();
}
public void someMethod() {
// Some update
changeIndicatorWrapper.set(!changeIndicatorWrapper.get());
}
}
Observable List
ObservableList<Element> elementsObservableList = FXCollections.observableList(
new ArrayList<>(),
(Element element) -> new Observable[] { element.changeIndicatorProperty() }
);
elementsObservableList.addListener(new ListChangeListener<Element>() {
#Override
public void onChanged(Change<? extends Element> c) {
System.out.println("CHANGE");
while(c.next()) {
if (c.wasUpdated()) {
for (int i = c.getFrom(); i < c.getTo(); ++i)
System.out.println(elementsObservableList.get(i));
}
}
}
});
My question is about this approach. Repeatedly set the changeIndicatorProperty to true not fire the change event. So, I need to reverse changeIndicatorProperty value changeIndicatorWrapper.set(!changeIndicatorWrapper.get()) each time. It is strange, isn't it?
Can I force programatically the update event?
It is strange, isn't it?
No this isn't surprising. For a change to be triggered a change needs to happen. If the BooleanProperty determines no change does happen and therefore the listeners are not notified of anything, this still satisfies the contract of Property.
Actually a Property isn't needed anyways. What is needed is a Observable that notifies it's observers. You could do this by using the following class and calling invalidate:
public class SimpleObservable implements Observable {
private final List<InvalidationListener> listeners = new LinkedList<>();
#Override
public void addListener(InvalidationListener listener) {
listeners.add(listener);
}
#Override
public void removeListener(InvalidationListener listener) {
listeners.remove(listener);
}
public void invalidate() {
for (InvalidationListener listener : listeners) {
try {
listener.invalidated(this);
} catch (RuntimeException ex) {
}
}
}
}
Example:
public class Element {
protected final SimpleObservable observable = new SimpleObservable();
public Observable getObservable() {
return observable;
}
public static <T extends Element> ObservableList<T> observableArrayList() {
return FXCollections.observableArrayList(e -> new Observable[]{e.observable});
}
private void update() {
observable.invalidate();
}
public static void main(String[] args) {
ObservableList<Element> list = Element.observableArrayList();
list.addListener((ListChangeListener.Change<? extends Element> c) -> {
while (c.next()) {
if (c.wasUpdated()) {
System.out.println("update: [" + c.getFrom() + ", " + c.getTo() + ")");
}
}
});
list.addAll(new Element(), new Element(), new Element());
list.get(1).update();
}
}