Hi I have a little problem in this Cell Factory :
private Callback<TableColumn<Member, String>,
TableCell<Member, String>> setPhotoCellFactory() {
Callback<TableColumn<Member, String>, TableCell<Member, String>> callback = new Callback<TableColumn<Member, String>, TableCell<Member, String>>() {
#Override
public TableCell<Member, String> call(TableColumn<Member, String> param) {
TableCell<Member, String> cell = new TableCell<Member, String>() {
#Override
protected void updateItem(String item, boolean empty) {
System.out.println(item);
super.updateItem(item, empty);
ImageView imageview = new ImageView();
imageview.setFitHeight(TABLE_IMAGE_MEMBER_HEIGHT);
imageview.setFitWidth(TABLE_IMAGE_MEMBER_WIDTH);
if(item != null) {
imageview.setImage(new Image(item));
setGraphic(imageview);
} else {
if(!empty) {
imageview.setImage(new Image(ImageFiles.GENERIC_MALE.toString()));
setGraphic(imageview);
} else {
setGraphic(null);
}
}
}
};
return cell;
}
};
return callback;
}
In Member object when Photo is null getter return Image corresponding a his gender. In this callback it seems that getter is not used.
How can I acces member property in this case ??
#EDIT
In Member class :
public String getPhoto() {
if (photo.getValue() != null && !photo.getValue().equals("")) {
return photo.get();
} else if (getGender().equals(Gender.F)) {
return ImageFiles.GENERIC_FEMALE.toString();
} else {
return ImageFiles.GENERIC_MALE.toString();
}
}
adding CellValueFactory :
this.simPhotoColumn.setCellValueFactory(data -> data.getValue().photoProperty());
OK I was found the solution. My error that I was searching how set image in CellFactory.
The response is to set custom CellValueFactory.
I was replaced this :
this.simPhotoColumn.setCellValueFactory(data -> data.getValue().photoProperty());
by :
this.memberPhoto.setCellValueFactory(getPhotoValueFactory());
private Callback<TableColumn.CellDataFeatures<Member, String>, ObservableValue<String>> getPhotoValueFactory() {
Callback<TableColumn.CellDataFeatures<Member, String>, ObservableValue<String>> callback = param -> new ReadOnlyObjectWrapper<>(param.getValue().getPhoto());
return callback;
}
Related
I'm trying to assign different context menus based on a parameter in the TreeItem class that I attach to the TreeItem. But I'm not having any success and I can't seem to find any info for how to do this. I've seen examples where a different context menu is used on the treeRoot then a different menu on the first branch but I can't find anything that would indicate how to have different context menus for the first then second branch in a TreeView.
I created these working classes to demonstrate where I'm at with this now, but it doesn't work as it never seems to actually pass the leaf nodes into the CellNode based on the output Im getting and the fact that the same menu is being applied to both levels of TreeItems.
Here is the GUI class:
public class MainUI {
public MainUI() {
makeControls();
ap = new AnchorPane(treeView);
Stage stage = new Stage();
Scene scene = new Scene(ap);
stage.setScene(scene);
stage.show();
stage.getScene().getWindow().centerOnScreen();
stage.getScene().getWindow().sizeToScene();
}
private final AnchorPane ap;
private TreeView treeView;
private TreeItem<TypeNode> treeRoot;
private void makeControls() {
treeView = new TreeView<>();
treeRoot = new TreeItem<>(new TypeNode());
treeView.setRoot(treeRoot);
treeView.setShowRoot(false);
treeView.setCellFactory(new CellFactory());
fillTree();
}
private void fillTree() {
new Thread(() -> {
for (int x=1; x <= 10; x++) {
TreeItem<TypeNode> branch = new TreeItem<>(new TypeNode(Type.TYPE_A));
for(int y=1; y <= 5; y++) {
TreeItem<TypeNode> leaf = new TreeItem<>(new TypeNode(Type.TYPE_B));
branch.getChildren().add(leaf);
}
treeRoot.getChildren().add(branch);
}
}).start();
}
}
The CellFactory:
public class CellFactory implements Callback<TreeView<TypeNode>, TreeCell<TypeNode>> {
#Override public TreeCell<TypeNode> call(TreeView<TypeNode> treeView) {
TreeCell<TypeNode> cell = new CellNode<>() {
#Override
public void updateItem(final TypeNode item, final boolean empty) {
super.updateItem(item, empty);
if (item == null) return;
setText(getTreeItem().getValue().getText());
setGraphic(getTreeItem().getGraphic());
}
};
return cell;
}
}
The CellNode:
public class CellNode<T> extends TreeCell<TypeNode> {
private final ContextMenu menuA = new ContextMenu();
private final ContextMenu menuB = new ContextMenu();
public CellNode() {
MenuItem menuA1 = new MenuItem("New A Item");
MenuItem menuA2 = new MenuItem("Copy A Item");
MenuItem menuB1 = new MenuItem("New B Item");
MenuItem menuB2 = new MenuItem("Copy B Item");
menuA.getItems().setAll(menuA1,menuA2);
menuB.getItems().setAll(menuB1,menuB2);
menuA1.setOnAction(e -> System.out.println("menuA1"));
menuA2.setOnAction(e -> System.out.println("menuA2"));
menuB1.setOnAction(e -> System.out.println("menuB1"));
menuB2.setOnAction(e -> System.out.println("menuB2"));
}
#Override
public void updateItem(TypeNode item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setText(null);
setGraphic(null);
} else {
if (!isEditing()) {
System.out.println(getTreeItem().getValue().getType());
if (!getTreeItem().isLeaf() && getTreeItem().getParent() != null) {
if(getTreeItem().getValue().getType().equals(Type.TYPE_A)) {
setContextMenu(menuA);
}
if(getTreeItem().getValue().getType().equals(Type.TYPE_B)) {
setContextMenu(menuB);
}
}
}
}
}
private String getString() {
return getItem() == null ? "" : getItem().toString();
}
}
TypeNode:
public class TypeNode {
private String text = "";
private final Type type;
private static int var = 0;
public TypeNode() {
this.type = Type.NO_TYPE;
}
public TypeNode(Type type) {
this.type = type;
var++;
this.text = this.type.toString() + ": " + var;
}
public String getText() {
return text;
}
public Type getType() {
return type;
}
#Override public String toString() {
return text;
}
}
And Type enum:
public enum Type {
TYPE_A,
TYPE_B,
NO_TYPE
}
Here is the output from the loading of the Scene:
TYPE_A
TYPE_A
TYPE_A
TYPE_A
TYPE_A
TYPE_A
TYPE_A
TYPE_A
TYPE_A
TYPE_A
TYPE_A
TYPE_A
TYPE_A
TYPE_A
And here you can see that menuB never gets assigned to anything:
CLEARLY, I'm doing something wrong, but I can't figure it out... any assistance or advice would be much appreciated.
Mike
After some discussion in the comments under my post, I ended up changing the updateItem method in the CellNode class to this, and it now works properly (Thanks to James_D for helping me spot what should have been obvious):
public void updateItem(TypeNode item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setText(null);
setGraphic(null);
setContextMenu(null);
}
else {
if (item != null) {
System.out.println(item.getType());
switch (item.getType()) {
case TYPE_A:
setContextMenu(menuA);
break;
case TYPE_B:
setContextMenu(menuB);
break;
default:
setContextMenu(null);
break;
}
}
else {
setContextMenu(null);
}
}
}
I'm trying to create a TextField whose content is validated with a template. To do this, I create a TextFormatter to which I pass a StringConverter.
However, I do notice a weird thing about using StringConverter<String>. When I enter invalid data and the field loses focus, it does not clear its content (it only clears it after subsequent focusing). For comparison, when I use StringConverter<LocalTime> this problem is not noticed.
If I catch the change of focus and validate the data, the problem is solved, but I wonder why there is a discrepancy in the validation in both cases.
public class Sample extends Application {
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) {
TextField fieldA = new TextField();
fieldA.setPromptText("00000");
fieldA.setTextFormatter(new TextFormatter<>(new StringConverter<String>() {
#Override
public String toString(String object) {
if(object == null) return "";
return object.matches("[0-9]{5}") ? object : "";
}
#Override
public String fromString(String string) {
if(string == null) return null;
return string.matches("[0-9]{5}") ? string : null;
}
}));
// fieldA.focusedProperty().addListener((observable, oldValue, newValue) -> {
// if(!fieldA.textProperty().getValueSafe().matches("[0-9]{5}")) {
// fieldA.setText(null);
// }
// });
TextField fieldB = new TextField();
fieldB.setPromptText("HH:MM:SS");
fieldB.setTextFormatter(new TextFormatter<>(new StringConverter<LocalTime>() {
#Override
public String toString(LocalTime object) {
if(object == null) return "";
return object.format(DateTimeFormatter.ofPattern("HH:mm:ss"));
}
#Override
public LocalTime fromString(String string) {
if(string == null) return null;
return LocalTime.parse(string, DateTimeFormatter.ofPattern("HH:mm:ss"));
}
}));
VBox vBox = new VBox(fieldA, fieldB);
vBox.setSpacing(5);
primaryStage.setScene(new Scene(vBox));
primaryStage.show();
}
}
ps: note that the purpose is not to create a TextField that can only accept 5 numbers. This is just an example.
I found the reason for the discrepancy in behavior. The main problem is that updating controls is done by binding valueProperty (in TextFormatter) with textProperty (in TextField). Because notifications of change to all Property objects are only saturated when the value of the wrapper is changed, sequential null submission causes a one-time notification.
The different behavior when using StringConverter<LocalTime> is because LocalTime::parse() throws a DateTimeParseException exception in invalid formatting. This in turn leads to a new valueProperty value being set, and to a previous valid control value.
This is the specific snippet of TextFormatter that is responsible for this behavior.
void updateValue(String text) {
if (!value.isBound()) {
try {
V v = valueConverter.fromString(text);
setValue(v);
} catch (Exception e) {
updateText(); // Set the text with the latest value
}
}
}
And the solution to the problem is that implementing StringConverter::fromString with an invalid value, instead of returning null, should throw unchecked exceptions.
public class Sample extends Application {
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) {
TextField fieldA = new TextField();
fieldA.setPromptText("00000");
fieldA.setTextFormatter(new TextFormatter<>(new StringConverter<String>() {
#Override
public String toString(String object) {
if(object == null) return "";
return object.matches("[0-9]{5}") ? object : "";
}
#Override
public String fromString(String string) {
if(string == null)
throw new RuntimeException("Value is null");
if(string.matches("[0-9]{5}")) {
return string;
}
throw new RuntimeException("Value not match");
}
}));
TextField fieldB = new TextField();
fieldB.setPromptText("HH:MM:SS");
fieldB.setTextFormatter(new TextFormatter<>(new StringConverter<LocalTime>() {
#Override
public String toString(LocalTime object) {
if(object == null) return "";
return object.format(DateTimeFormatter.ofPattern("HH:mm:ss"));
}
#Override
public LocalTime fromString(String string) {
if(string == null) return null;
return LocalTime.parse(string, DateTimeFormatter.ofPattern("HH:mm:ss"));
}
}));
VBox vBox = new VBox(fieldA, fieldB);
vBox.setSpacing(5);
primaryStage.setScene(new Scene(vBox));
primaryStage.show();
}
}
I have method that make a TableColumn editable that I made according to this tutorial, with the two changes recommended in this question witch are changing focusedProperty with setOnAction and add binding. The method is like this:
public static <T> void setAutoCompleteTableColumn(TableColumn<T,String> column, BiConsumer<T, String> field, String type, BiConsumer handleUpdates, List autoComplete){
column.setCellFactory(param -> {
return new TableCell<T, String>(){
private TextField textField;
#Override
public void startEdit() {
super.startEdit();
createTextField();
setText(null);
setGraphic(textField);
textField.selectAll();
}
#Override
public void cancelEdit() {
super.cancelEdit();
setText((String) getItem());
setGraphic(null);
}
#Override
protected void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setText(null);
setGraphic(null);
} else {
if (isEditing()) {
if (textField != null) {
textField.setText(getString());
}
setText(null);
setGraphic(textField);
} else {
setText(getString());
setGraphic(null);
}
}
}
private void createTextField(){
textField = new TextField(getString());
textField.setMinWidth(this.getWidth() - this.getGraphicTextGap()* 2);
textField.setOnAction(event ->{
if(textField.getText() != ""){
commitEdit(textField.getText());
}
});
AutoCompletionBinding<T> binding = TextFields.bindAutoCompletion(textField,autoComplete);
}
private String getString() {
return getItem() == null ? "" : getItem().toString();
}
};
});
column.setOnEditCommit(event -> {
T model = event.getRowValue();
if (!event.getNewValue().isEmpty()) {
Cloner cloner = new Cloner();
final T oldModel = cloner.deepClone(model);
//Update the old value with the new value
field.accept(model, event.getNewValue());
//Retrieve changes
handleUpdates.accept(oldModel, model);
}
});
}
I use this method in the controller like this:
TableColumn<Purchase, String> nameColumn = new TableColumn<>();
setAutoCompleteTableColumn(nameColumn, (p,s) -> validatePurchaseEdit(p,s), "text", (o, n) -> updateTracker(o, n), appState.items);
I have a button click event that switch the TableView editable to false.
For some reason when this event is executed the current cell that I am editing does commit edit but an other cell(a random one) switches to edit mode. Can some one explain what is happening, and how can I make all cells commit edit?
I have TableView with column inside it that must only accept numbers.
and I added onMouseClickListener to enter edit mode on the mouse click instead of double click on the cell
I want a way to not allowing the user to enter any character except numbers. My code is:
Callback<TableColumn<DailyDetails, String>, TableCell<DailyDetails, String>> defaultCellFactory
= TextFieldTableCell.<DailyDetails>forTableColumn();
dailyCredit.setCellFactory(column -> {
TableCell<DailyDetails, String> cell = defaultCellFactory.call(column);
cell.setOnMouseClicked(e -> {
if (!cell.isEditing() && !cell.isEmpty()) {
cell.getTableView().edit(cell.getIndex(), column);
}
});
return cell;
});
I implemented Table cell from the scratch:
class NumberCell extends TableCell<DailyDetails, String> {
private TextField textField;
public NumberCell() {
}
#Override
public void startEdit() {
super.startEdit();
if (textField == null) {
createTextField();
}
setGraphic(textField);
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
textField.selectAll();
}
#Override
public void cancelEdit() {
super.cancelEdit();
setText(String.valueOf(getItem()));
setContentDisplay(ContentDisplay.TEXT_ONLY);
}
#Override
public void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setText(null);
setGraphic(null);
} else {
if (isEditing()) {
if (textField != null) {
textField.setText(getString());
}
setGraphic(textField);
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
} else {
setText(getString());
setContentDisplay(ContentDisplay.TEXT_ONLY);
}
}
}
private void createTextField() {
textField = new TextField(getString());
//textField.setMinWidth(this.getWidth() - this.getGraphicTextGap() * 2);
textField.lengthProperty().addListener(new ChangeListener<Number>(){
#Override
public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) {
if (newValue.intValue() > oldValue.intValue()) {
char ch = textField.getText().charAt(oldValue.intValue());
// Check if the new character is the number or other's
if (!(ch >= '0' && ch <= '9' )) {
// if it's not number then just setText to previous one
textField.setText(textField.getText().substring(0,textField.getText().length()-1));
}
}
}
});
}
private String getString() {
return getItem() == null ? "" : getItem().toString();
}
}
Callback<TableColumn<DailyDetails, String>,
TableCell<DailyDetails, String>> cellFactory
= (TableColumn<DailyDetails, String> p) -> new NumberCell();
dailyDebit.setCellFactory(cellFactory);
the problem is i lost the on mouse listener cell.setOnMouseClicked!!!
how do i get the cell again to assign the listener ???
Just for driving the new api into everybody's brain: a full example with a slightly different TextFormatter (than in the other answer) that is Locale-aware and (dirtily!) hooked into core TextFieldTableCell, can be used in any custom editing TableCell as well:
/**
* Example of how-to use a TextFormatter in a editing TableCell.
*/
public class CellFormatting extends Application {
private Parent getContent() {
ObservableList<IntData> data = FXCollections.observableArrayList(
new IntData(1), new IntData(2), new IntData(3)
);
TableView<IntData> table = new TableView<>(data);
table.setEditable(true);
TableColumn<IntData, Integer> column = new TableColumn<>("Data");
column.setCellValueFactory(new PropertyValueFactory("data"));
// core default: will throw exception on illegal values
// column.setCellFactory(TextFieldTableCell.forTableColumn(new IntegerStringConverter()));
NumberFormat format = NumberFormat.getIntegerInstance();
UnaryOperator<TextFormatter.Change> filter = c -> {
if (c.isContentChange()) {
ParsePosition parsePosition = new ParsePosition(0);
// NumberFormat evaluates the beginning of the text
format.parse(c.getControlNewText(), parsePosition);
if (parsePosition.getIndex() == 0 ||
parsePosition.getIndex() < c.getControlNewText().length()) {
// reject parsing the complete text failed
return null;
}
}
return c;
};
column.setCellFactory(c -> new ValidatingTextFieldTableCell<>(
// note: each cell needs its own formatter
// see comment by #SurprisedCoconut
new TextFormatter<Integer>(
// note: should use local-aware converter instead of core!
new IntegerStringConverter(), 0,
filter)));
table.getColumns().add(column);
VBox box = new VBox(table);
return box;
}
/**
* TextFieldTableCell that validates input with a TextFormatter.
* <p>
* Extends TextFieldTableCell, accesses super's private field reflectively.
*
*/
public static class ValidatingTextFieldTableCell<S, T> extends TextFieldTableCell<S, T> {
private TextFormatter<T> formatter;
private TextField textAlias;
public ValidatingTextFieldTableCell() {
this((StringConverter<T>)null);
}
public ValidatingTextFieldTableCell(StringConverter<T> converter) {
super(converter);
}
public ValidatingTextFieldTableCell(TextFormatter<T> formatter) {
super(formatter.getValueConverter());
this.formatter = formatter;
}
/**
* Overridden to install the formatter. <p>
*
* Beware: implementation detail! super creates and configures
* the textField lazy on first access, so have to install after
* calling super.
*/
#Override
public void startEdit() {
super.startEdit();
installFormatter();
}
private void installFormatter() {
if (formatter != null && isEditing() && textAlias == null) {
textAlias = invokeTextField();
textAlias.setTextFormatter(formatter);
}
}
private TextField invokeTextField() {
Class<?> clazz = TextFieldTableCell.class;
try {
Field field = clazz.getDeclaredField("textField");
field.setAccessible(true);
return (TextField) field.get(this);
} catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException e) {
e.printStackTrace();
}
return null;
}
}
public static class IntData {
IntegerProperty data = new SimpleIntegerProperty(this, "data");
public IntData(int value) {
setData(value);
}
public void setData(int value) {
data.set(value);
}
public int getData() {
return data.get();
}
public IntegerProperty dataProperty() {
return data;
}
}
#Override
public void start(Stage primaryStage) throws Exception {
primaryStage.setScene(new Scene(getContent()));
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
BTW, the formatter is re-used from another question where the task at hand was to restrict input into a Spinner.
Use a TextFormatter on the TextField like this:
TextFormatter<String> formatter = new TextFormatter<String>( change -> {
change.setText(change.getText().replaceAll("[^0-9.,]", ""));
return change;
});
textField.setTextFormatter(formatter);
Works with Java8u40 upwards. Use e. g. the TableView example from the Oracle site as base.
I am using an ObservableList to control the items and every time I delete an item, the TableView removes it from the datasource. But the view is not being updated as I'm still seeing all the items. The only difference is that the removed item can not be selected anymore.
Similar problem: javafx listview and treeview controls are not repainted correctly
In the following the code:
final TableColumn<TMachineType, String> testerType = new TableColumn<TMachineType, String>(
bundle.getString("table.testerType"));
testerType
.setCellValueFactory(new PropertyValueFactory<TMachineType, String>(
"testerType"));
testerType
.setCellFactory(new Callback<TableColumn<TMachineType, String>, TableCell<TMachineType, String>>() {
#Override
public TableCell<TMachineType, String> call(
final TableColumn<TMachineType, String> param) {
final TableCell<TMachineType, String> cell = new TableCell<TMachineType, String>() {
#Override
protected void updateItem(final String item,
final boolean empty) {
super.updateItem(item, empty);
textProperty().unbind();
if (empty || item == null) {
setGraphic(null);
setText(null);
}
if (!empty) {
final TMachineType row = (TMachineType) getTableRow().getItem();
textProperty().bind(
row.testerTypeProperty());
}
}
};
return cell;
}
});
TMachineType class:
private final SimpleStringProperty testerType = new SimpleStringProperty();
#Column(name = "TESTER_TYPE")
public String getTesterType() {
return testerType.get();
}
public void setTesterType(String testerType) {
this.testerType.set(testerType);
}
public StringProperty testerTypeProperty() {
return testerType;
}
Observable List:
1. DB entities:
final Query q = em.createQuery("SELECT t FROM TMachineType t");
final List resultList = q.getResultList();
2. Obs. list setup:
ObservableList<TMachineType> observableList;
...
observableList = FXCollections.observableArrayList(resultList);
tableMachineType.setItems(observableList);
FXCollections.sort(observableList);
Row removal:
#FXML
private void handleOnRemove(final ActionEvent event) {
final ObservableList<TMachineType> selectedIndices = tableMachineType
.getSelectionModel().getSelectedItems();
final String infoTxt = selectedIndices.size() + " "
+ bundle.getString("message.records_removed");
final List<TMachineType> deleteBuffer = new ArrayList<TMachineType>();
deleteBuffer.addAll(selectedIndices);
for (final TMachineType selectedIdx : deleteBuffer) {
selectedIdx.manufacturerProperty().unbind();
selectedIdx.testerTypeProperty().unbind();
deleted.add(selectedIdx);
observableList.remove(selectedIdx);
// tableMachineType.getItems().remove(selectedIdx);
}
..
}
removing the cellFactory fix this problem ! YEHU :)
testerType
.setCellFactory(new Callback<TableColumn<TMachineType, String>, TableCell<TMachineType, String>>() {
#Override
public TableCell<TMachineType, String> call(
final TableColumn<TMachineType, String> param) {
final TableCell<TMachineType, String> cell = new TableCell<TMachineType, String>() {
#Override
protected void updateItem(final String item,
final boolean empty) {
super.updateItem(item, empty);
textProperty().unbind();
if (empty || item == null) {
setGraphic(null);
setText(null);
}
if (!empty) {
final TMachineType row = (TMachineType) getTableRow().getItem();
textProperty().bind(
row.testerTypeProperty());
}
}
};
return cell;
}
});