Issue with creating a JFX TableView with custom editable cell - javafx

I'm trying to create a TableView which contains Student objects and Classroom objects. Now, I want to have a ChoiceBox to swap Students from one Classroom to another.
Here's the code
public class ChoiceBoxCell extends TableCell<Student, Classroom> {
ChoiceBox<Classroom> classroomChoiceBox = new ChoiceBox<>();
public ChoiceBoxCell(ObservableList<Classroom> classroomObservableList) {
ObservableList<Classroom> classroomObservableListList = classroomObservableList;
classroomChoiceBox.setItems(classroomObservableListList);
classroomChoiceBox.getSelectionModel().selectedIndexProperty().addListener((obs, oldValue, newValue) -> {
Classroom value = classroomChoiceBox.getItems().get((int) newValue);
classroomChoiceBox.setValue(value);
processEdit(value);
});
}
private void processEdit(Classroom value) {
commitEdit(value);
classroomChoiceBox.setValue(value);
setGraphic(classroomChoiceBox);
}
#Override
public void cancelEdit() {
super.cancelEdit();
setGraphic(classroomChoiceBox);
}
#Override
public void commitEdit(Classroom value) {
super.commitEdit(value);
classroomChoiceBox.setValue(value);
setGraphic(classroomChoiceBox);
}
#Override
public void startEdit() {
super.startEdit();
Classroom value = getItem();
if (value != null) {
classroomChoiceBox.setValue(value);
setGraphic(classroomChoiceBox);
}
}
#Override
protected void updateItem(Classroom item, boolean empty) {
super.updateItem(item, empty);
if (item == null || empty) {
classroomChoiceBox.setValue(item);
setGraphic(classroomChoiceBox);
} else {
classroomChoiceBox.setValue(item);
setGraphic(classroomChoiceBox);
}
}
}
In my TableView class
List classroomList = new ClassroomDao().getAllClasses();
ObservableList classroomObservableList = FXCollections.observableArrayList(classroomList);
classroomNameColumn.setPrefWidth(columnSize);
classroomNameColumn.setCellValueFactory(cdf -> cdf.getValue().classroomProperty());
classroomNameColumn.setCellFactory(column -> new ChoiceBoxCell(classroomObservableList));
classroomNameColumn.setEditable(true);
I'm getting the following exception
Exception in thread "JavaFX Application Thread" java.lang.ArrayIndexOutOfBoundsException: -1
at java.util.ArrayList.elementData(ArrayList.java:418)
at java.util.ArrayList.get(ArrayList.java:431)
at com.sun.javafx.collections.ObservableListWrapper.get(ObservableListWrapper.java:89)
at view.adminAccess.studentOverview.ChoiceBoxCell.lambda$new$0(ChoiceBoxCell.java:20)
at com.sun.javafx.binding.ExpressionHelper$Generic.fireValueChangedEvent(ExpressionHelper.java:361)
at com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:81)
at javafx.beans.property.ReadOnlyIntegerWrapper$ReadOnlyPropertyImpl.fireValueChangedEvent(ReadOnlyIntegerWrapper.java:176)
at javafx.beans.property.ReadOnlyIntegerWrapper.fireValueChangedEvent(ReadOnlyIntegerWrapper.java:142)
at javafx.beans.property.IntegerPropertyBase.markInvalid(IntegerPropertyBase.java:113)
at javafx.beans.property.IntegerPropertyBase.set(IntegerPropertyBase.java:147)
at javafx.scene.control.SelectionModel.setSelectedIndex(SelectionModel.java:68)
at javafx.scene.control.SingleSelectionModel.select(SingleSelectionModel.java:114)
at javafx.scene.control.ChoiceBox$4.invalidated(ChoiceBox.java:331)
at javafx.beans.property.ObjectPropertyBase.markInvalid(ObjectPropertyBase.java:111)
at javafx.beans.property.ObjectPropertyBase.set(ObjectPropertyBase.java:146)
at javafx.scene.control.ChoiceBox.setValue(ChoiceBox.java:336)
at view.adminAccess.studentOverview.ChoiceBoxCell.updateItem(ChoiceBoxCell.java:58)
at view.adminAccess.studentOverview.ChoiceBoxCell.updateItem(ChoiceBoxCell.java:10)
at javafx.scene.control.TableCell.updateItem(TableCell.java:639)
at javafx.scene.control.TableCell.indexChanged(TableCell.java:468)
at javafx.scene.control.IndexedCell.updateIndex(IndexedCell.java:116)
at com.sun.javafx.scene.control.skin.TableRowSkinBase.requestCellUpdate(TableRowSkinBase.java:659)
at com.sun.javafx.scene.control.skin.TableRowSkinBase.lambda$init$497(TableRowSkinBase.java:159)
at com.sun.javafx.binding.ExpressionHelper$SingleInvalidation.fireValueChangedEvent(ExpressionHelper.java:137)
at com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:81)
at javafx.beans.property.ObjectPropertyBase.fireValueChangedEvent(ObjectPropertyBase.java:105)
at javafx.beans.property.ObjectPropertyBase.markInvalid(ObjectPropertyBase.java:112)
at javafx.beans.property.ObjectPropertyBase.set(ObjectPropertyBase.java:146)
at javafx.scene.control.Cell.setItem(Cell.java:403)
at javafx.scene.control.Cell.updateItem(Cell.java:670)
at javafx.scene.control.TableRow.updateItem(TableRow.java:268)
at javafx.scene.control.TableRow.indexChanged(TableRow.java:225)
at javafx.scene.control.IndexedCell.updateIndex(IndexedCell.java:116)
at com.sun.javafx.scene.control.skin.VirtualFlow.releaseCell(VirtualFlow.java:1807)
at com.sun.javafx.scene.control.skin.VirtualFlow.getCellLength(VirtualFlow.java:1881)
at com.sun.javafx.scene.control.skin.VirtualFlow.computeViewportOffset(VirtualFlow.java:2528)
at com.sun.javafx.scene.control.skin.VirtualFlow.layoutChildren(VirtualFlow.java:1189)
at javafx.scene.Parent.layout(Parent.java:1079)
at javafx.scene.Parent.layout(Parent.java:1085)
at javafx.scene.Parent.layout(Parent.java:1085)
at javafx.scene.Parent.layout(Parent.java:1085)
at javafx.scene.Parent.layout(Parent.java:1085)
at javafx.scene.Scene.doLayoutPass(Scene.java:552)
at javafx.scene.Scene$ScenePulseListener.pulse(Scene.java:2397)
at com.sun.javafx.tk.Toolkit.lambda$runPulse$31(Toolkit.java:355)
at java.security.AccessController.doPrivileged(Native Method)
at com.sun.javafx.tk.Toolkit.runPulse(Toolkit.java:354)
at com.sun.javafx.tk.Toolkit.firePulse(Toolkit.java:381)
at com.sun.javafx.tk.quantum.QuantumToolkit.pulse(QuantumToolkit.java:510)
at com.sun.javafx.tk.quantum.PaintCollector.liveRepaintRenderJob(PaintCollector.java:320)
at com.sun.javafx.tk.quantum.GlassViewEventHandler$ViewEventNotification.run(GlassViewEventHandler.java:788)
at com.sun.javafx.tk.quantum.GlassViewEventHandler$ViewEventNotification.run(GlassViewEventHandler.java:749)
at java.security.AccessController.doPrivileged(Native Method)
at com.sun.javafx.tk.quantum.GlassViewEventHandler.lambda$handleViewEvent$369(GlassViewEventHandler.java:828)
at com.sun.javafx.tk.quantum.QuantumToolkit.runWithoutRenderLock(QuantumToolkit.java:389)
at com.sun.javafx.tk.quantum.GlassViewEventHandler.handleViewEvent(GlassViewEventHandler.java:827)
at com.sun.glass.ui.View.handleViewEvent(View.java:539)
at com.sun.glass.ui.View.notifyResize(View.java:875)
at com.sun.glass.ui.win.WinWindow._setBounds(Native Method)
at com.sun.glass.ui.Window.setBounds(Window.java:572)
at com.sun.javafx.tk.quantum.WindowStage.setBounds(WindowStage.java:318)
at javafx.stage.Window$TKBoundsConfigurator.apply(Window.java:1274)
at javafx.stage.Window$TKBoundsConfigurator.pulse(Window.java:1290)
at com.sun.javafx.tk.Toolkit.lambda$runPulse$31(Toolkit.java:355)
at java.security.AccessController.doPrivileged(Native Method)
at com.sun.javafx.tk.Toolkit.runPulse(Toolkit.java:354)
at com.sun.javafx.tk.Toolkit.firePulse(Toolkit.java:378)
at com.sun.javafx.tk.quantum.QuantumToolkit.pulse(QuantumToolkit.java:510)
at com.sun.javafx.tk.quantum.QuantumToolkit.pulse(QuantumToolkit.java:490)
at com.sun.javafx.tk.quantum.QuantumToolkit.lambda$runToolkit$405(QuantumToolkit.java:319)
at com.sun.glass.ui.InvokeLaterDispatcher$Future.run(InvokeLaterDispatcher.java:95)
at com.sun.glass.ui.win.WinApplication._runLoop(Native Method)
at com.sun.glass.ui.win.WinApplication.lambda$null$149(WinApplication.java:191)
at java.lang.Thread.run(Thread.java:745)
This is how it looks like. It just seems like the last cell is trying to fit every single row in the table with a ChoiceBox.
How can I keep this limited only to the rows which have data in them?
EDIT: additional issue
Part of the code I'm using now.
classroomChoiceBox.getSelectionModel().selectedItemProperty().addListener((obs, oldValue, newValue) -> {
if (newValue != null) {
processEdit(newValue);
}
});
#Override
public void commitEdit(Classroom value) { // always gets executed
super.commitEdit(value);
Student student = (Student) getTableRow().getItem();
student.setClassroom(value);
new StudentDao().updateStudent(student); // students get updated on creation of the table
classroomChoiceBox.setValue(value);
setGraphic(classroomChoiceBox);
}
The issue is that each Student gets updated when the table is created.

The selected index in a selection model is set to -1 if nothing is selected (see docs). Since -1 is not a valid index in a list, you need to check for this case:
classroomChoiceBox.getSelectionModel().selectedIndexProperty().addListener((obs, oldValue, newValue) -> {
int index = newValue.intValue();
if (index >= 0) {
Classroom value = classroomChoiceBox.getItems().get(index);
// what is the point of the next line?
// surely this is the value in the choice box already???
classroomChoiceBox.setValue(value);
processEdit(value);
}
});
Of course, it may be easier just to get the value directly from the selection model, instead of getting its index:
classroomChoiceBox.getSelectionModel().selectedItemProperty().addListener((obs, oldValue, newValue) -> {
if (newValue != null) { /* don't know if you care if this is null... */
processEdit(newValue);
}
});
The reason you are seeing choice boxes in the empty cells is that you explicitly set the graphic to the choice box in empty cells, in the cell's updateItem method. I assume (since the if block is identical to the else block) that this is just a copy-and-paste error of some kind.

Related

How do I make a generic TableColumn renderer for JavaFX TableView column

In a javaFX view controller, initialize method, I have a TableView column renderer that shows financial amounts, with each cell being rendered with an amount and in red or blue color depending on whether the amount is a debit or credit amount.
#Override
public void initialize(URL url, ResourceBundle rb)
budgetAmountCol.setCellValueFactory(cellData -> cellData.getValue().budgetAmountProperty());
// Custom rendering of the budget amount cells in the column.
budgetAmountCol.setCellFactory((TableColumn<TxnObject, String> column) -> {
return new TableCell<TxnObject, String>() {
#Override
protected void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if (item == null || empty) {
}
else {
double amt = Double.parseDouble(item);
if (amt == 0) {
setId("zero-amt");
} else {
if (amt < 0){
item = item.substring(1, item.length());
setId("debit-amt");
} else {
setId("credit-amt");
}
}
}
setText(item);
}
};
});
Because I have several columns that need to be rendered similarily I am trying to avoid copious amounts of source code and turn the above into a single method that can be passed parameters and thus render the columns, something like
private void renderColumn(..) { }
The variables that occupy is table cell in the column are defined in an object definition like this:
public class TxnObject {
..
private final StringProperty budgetAmount;
..
public String getbudgetAmount() {
double d = Double.parseDouble(budgetAmount.get());
setbudgetAmount(MainApp.formatAmount(d));
return budgetAmount.get();
}
public void setbudgetAmount(String budgetAmount) {
this.budgetAmount.set(budgetAmount);
}
public StringProperty budgetAmountProperty() {
return budgetAmount;
}
The requirement is therefore to pass the content of the second line of the code, i.e.
budgetAmountCol.setCellValueFactory(cellData -> cellData.getValue().budgetAmountProperty());
to render column: So at a minimum the method needs the table column and the variable property:
private void renderColumn(TableColumn<TxnObject, String> tcol, StringProperty sprop) {
node.setCellValueFactory(..)
}
But I can’t get the correct call to the method. I have tried the following with the indicated result:
renderColumn(budgetAmountCol, budgetAmountProperty());
syntax error: no method BudgetAmountProperty()
renderColumn(budgetAmountCol, cellData.getValue().budgetAmountProperty());
syntax error: Cannot find symbol CellData
renderColumn(budgetAmountCol, cellData -> cellData.getValue().budgetAmountProperty());
syntax error: StringProperty is not a functional interface
Im finding the syntax and understanding of how to achieve my object rather challenging and would appreciate if I could get some suggestions to try and achieve a solution.
The only way to "pass a method" is by creating a object containing the logic to do this or by using a method reference.
Furthermore even though JavaFX does not enforce those restrictions, id should be unique. Use pseudoclasses instead.
You could use
private static final PseudoClass ZERO = PseudoClass.getPseudoClass("zero-atm");
private static final PseudoClass CREDIT = PseudoClass.getPseudoClass("credit-atm");
private static final PseudoClass DEBIT = PseudoClass.getPseudoClass("debit-atm");
public static <S, T> void renderColumn(TableColumn<S, T> column,
final Callback<? super S, ObservableValue<T>> extractor, final Callback<? super T, String> converter,
final Comparator<T> comparator, final T zero) {
if (extractor == null || comparator == null || zero == null) {
throw new IllegalArgumentException();
}
column.setCellValueFactory(cd -> extractor.call(cd.getValue()));
column.setCellFactory(col -> new TableCell<S, T>() {
#Override
protected void updateItem(T item, boolean empty) {
super.updateItem(item, empty);
pseudoClassStateChanged(ZERO, false);
pseudoClassStateChanged(CREDIT, false);
pseudoClassStateChanged(DEBIT, false);
if (empty || item == null) {
setText("");
} else {
setText(converter.call(item));
int comparison = comparator.compare(zero, item);
if (comparison == 0) {
pseudoClassStateChanged(ZERO, true);
} else {
pseudoClassStateChanged(comparison < 0 ? CREDIT : DEBIT, true);
}
}
}
});
}
public static <S, T extends Comparable<T>> void renderColumn(TableColumn<S, T> column,
Callback<? super S, ObservableValue<T>> extractor, Callback<? super T, String> converter, T zero) {
renderColumn(column, extractor, converter, Comparator.naturalOrder(), zero);
}
Assuming you change the type of the budget to ObjectProperty<BigDecimal>, the method can be invoked like as shown below. (BigDecimal is a lot easier to work with when it comes to comparing values and doing other mathematical operation in addition to avoiding rounding errors.) Otherwise you could simply hardcode the second type parameter and the other functionality and only keep the first 2 parameters of the method.
renderColumn(column, TxnObject::budgetAmountProperty, (BigDecimal val) -> val.abs().toString(), BigDecimal.ZERO);

Add buttons to the row currently being edited

I have tried searching both google and stackoverflow for answer to my question but I could not find any.
I have a program that adds/reads data from a database and to a tableview. I want to be able to edit the previously entered data from the tableview by adding two buttons (Save and abort) to the current row being edited.
I am having some troubles understanding the routines like Callback etc. But I have managed to get a column with two buttons to appear when I start the edit. But I get buttons on every column I just want the buttons on the currently
selected row. Also I don't really understand how to get the current object from the save-button to be able to save it.
And also how the abort-button should work to cancel all changes.
public void setUpTableView() {
columnAnkomstdatum.setCellValueFactory(new PropertyValueFactory<>("arrivalDate"));
columnSupplier.setCellValueFactory(new PropertyValueFactory<>("supplier"));
columnRadiopharmaceutical.setCellValueFactory(new PropertyValueFactory<>("radiopharmaceutical"));
columnActivity.setCellValueFactory(new PropertyValueFactory<>("startActivity"));
columnCalibrationdate.setCellValueFactory(new PropertyValueFactory<>("startDate"));
columnBatchNumber.setCellValueFactory(new PropertyValueFactory<>("batchNumber"));
columnContaminationControl.setCellValueFactory(new PropertyValueFactory<>("contaminationControll"));
columnRoom.setCellValueFactory(new PropertyValueFactory<>("room"));
columnUser.setCellValueFactory(new PropertyValueFactory<>("user"));
tableview.setEditable(true);
columnSupplier.setEditable(true);
columnSupplier.setCellFactory(ComboBoxTableCell.forTableColumn(supplierList));
columnSupplier.setOnEditCommit(t -> {
ArrayList<Radiopharmaceutical> radioListfromSupplier = new RadiopharmaceuticalDao().getRadiopharmaceuticalsBySupplierName(t.getNewValue().getSupplierName());
radioList = FXCollections.observableArrayList(radioListfromSupplier);
t.getRowValue().setSupplier(t.getNewValue());
columnRadiopharmaceutical.setCellFactory(ComboBoxTableCell.forTableColumn(radioList));
if(tableview.getColumns().size() <= 9) {
addButtonsToTable();
}
});
}
private void addButtonsToTable() {
TableColumn<RegRadio, Void> editRow = new TableColumn<>("Edit");
tableview.getColumns().add(editRow);
Callback<TableColumn<RegRadio, Void>, TableCell<RegRadio, Void>> cellFactory = new Callback<TableColumn<RegRadio,Void>, TableCell<RegRadio,Void>>() {
#Override
public TableCell<RegRadio, Void> call(final TableColumn<RegRadio, Void> param) {
final TableCell<RegRadio, Void> cell = new TableCell<RegRadio, Void>() {
private final Button btnSave = new Button("Save");
private final Button btnAbort = new Button("Avbryt");
{
btnSave.setOnAction((ActionEvent event) -> {
RegRadio rr = getTableView().getItems().get(getIndex());
System.out.println("Saved");
});
}
{
btnAbort.setOnAction((ActionEvent event) -> {
System.out.println("Abort");
});
}
#Override
public void updateItem(Void item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setGraphic(null);
} else {
HBox pane = new HBox(btnSave, btnAbort);
setGraphic(pane);
}
}
};
return cell;
}
};
editRow.setCellFactory(cellFactory);
tableview.getColumns().add(editRow);
}
In your updateItem callback you can check if the cell is in the selected row in order to decide if you should show the buttons or not. Additionally you also need a flag to check if the user is editing. Something like this:
#Override
public void updateItem(Void item, boolean empty) {
super.updateItem(item, empty);
var selectedCells = tableview.getSelectionModel().getSelectedCells();
if (empty || !isEditing || selectedCells.isEmpty || getTableRow().getIndex() != selectedCells.get(0).getRow()) {
setGraphic(null);
} else {
HBox pane = new HBox(btnSave, btnAbort);
setGraphic(pane);
}
}
Another approach would be to change the visibility of the buttons depending on whether the cell is in the selected row:
#Override
public void updateItem(Void item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setGraphic(null);
} else {
HBox pane = new HBox(btnSave, btnAbort);
var selectedCells = tableview.getSelectionModel().getSelectedCells();
pane.setVisible(!selectedCells.isEmpty() && getTableRow().getIndex() == selectedCells.get(0).getRow())
setGraphic(pane);
}
}
(I haven't compiled and tested these code samples)

JAVAFX - Tableview with modifiable custum TimePicker cell close too quickly

I have a Tableview with modifiable custum TimePicker cell.
I wrote a function that listens the changes in that cell but when I click on a specific hour, the clock doesn't stay open and close at the first click and i have to click again to select the minutes for exemple.
How can i let the clock open and make the editCommitEvent() when the clock close?
Thank you for your help :)
Here is the code of my custum cell.
PS: I use jfoenix TimePicker
public class TimePickerTableCell<Patient> extends TableCell<Patient, LocalTime> {
private JFXTimePicker timePicker;
private boolean listening = true;
// listener for changes in the timePicker
#SuppressWarnings({ "unchecked", "rawtypes" })
private final ChangeListener<LocalTime> listener = (observable, oldValue, newValue) -> {
if (listening) {
listening = false;
TableColumn<Patient, LocalTime> column = getTableColumn();
EventHandler<TableColumn.CellEditEvent<Patient, LocalTime>> handler = column.getOnEditCommit();
if (handler != null) {
// use TableColumn.onEditCommit if there is a handler
handler.handle(new TableColumn.CellEditEvent<>(
(TableView<Patient>) getTableView(),
new TablePosition<Patient, LocalTime>(getTableView(), getIndex(), column),
TableColumn.<Patient, LocalTime>editCommitEvent(),
newValue
));
} else {
// otherwise check if ObservableValue from cellValueFactory is
// also writable and use in that case
ObservableValue<LocalTime> observableValue = column.getCellObservableValue((Patient) getTableRow().getItem());
if (observableValue instanceof WritableValue) {
((WritableValue) observableValue).setValue(newValue);
}
}
listening = true;
}
};
public TimePickerTableCell () {
this.timePicker = new JFXTimePicker();
this.timePicker.valueProperty().addListener(listener);
this.timePicker.setOnMouseEntered((event)->{timePicker.requestFocus();timePicker.show();System.err.println("OUVERTURE TIMEPICKER");});
this.timePicker.setOnMouseExited((event)->{if(event.getY()<23)timePicker.hide();});
}
#Override
protected void updateItem(LocalTime item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
listening = false;
setGraphic(null);
} else {
listening = false;
setGraphic(this.timePicker);
this.timePicker.setValue(item);
this.timePicker.getStyleClass().add("time-picker");
listening = true;
}
}
public static <E> Callback<TableColumn<E, LocalTime>, TableCell<E, LocalTime>> forTableColumn() {
return column -> new TimePickerTableCell<>();
}
}
First, the reason the JFXTimePicker hides when you go to click the clock is (most likely) because of your onMouseExited handler. When you move your mouse over the popup it "exits" the JFXTimePicker and thus hides the clock.
You're also implementing an editable TableCell the wrong way. You should be overriding the startEdit() and cancelEdit() methods of the Cell class (which TableCell inherits from). You can look at the source code of classes like TextFieldTableCell for how it's done. I also worked up an example for doing this with JFXTimePicker:
import com.jfoenix.controls.JFXTimePicker;
import javafx.application.Platform;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.input.KeyCode;
import javafx.util.Callback;
import javafx.util.converter.LocalTimeStringConverter;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
public class TimePickerTableCell<S> extends TableCell<S, LocalTime> {
// Static methods for creating TableColumn.cellFactory Callbacks
public static <S> Callback<TableColumn<S, LocalTime>, TableCell<S, LocalTime>> forTableColumn() {
return v -> new TimePickerTableCell<>();
}
public static <S> Callback<TableColumn<S, LocalTime>, TableCell<S, LocalTime>> forTableColumn(DateTimeFormatter formatter) {
return v -> new TimePickerTableCell<>(formatter);
}
// Formatter property
private final ObjectProperty<DateTimeFormatter> formatter = new SimpleObjectProperty<>(this, "formatter");
public final void setFormatter(DateTimeFormatter formatter) { this.formatter.set(formatter); }
public final DateTimeFormatter getFormatter() { return formatter.get(); }
public final ObjectProperty<DateTimeFormatter> formatterProperty() { return formatter; }
// JFXTimePicker field
private JFXTimePicker timePicker;
// Constructors
public TimePickerTableCell() {
this(DateTimeFormatter.ISO_LOCAL_TIME);
}
public TimePickerTableCell(DateTimeFormatter formatter) {
getStyleClass().add("time-picker-table-cell");
setFormatter(formatter);
}
// Display logic
#Override
protected void updateItem(LocalTime item, boolean empty) {
super.updateItem(item, empty);
setGraphic(null);
if (empty || item == null) {
setText(null);
} else {
setText(formatItem(item));
}
}
private String formatItem(LocalTime item) {
if (item == null) {
return null;
}
return getFormatter() == null ? item.toString() : getFormatter().format(item);
}
// Edit logic
#Override
public void startEdit() {
if (!isEditable() ||
!getTableColumn().isEditable() ||
!getTableView().isEditable()) {
return;
}
super.startEdit();
if (isEditing()) {
if (timePicker == null) {
createTimePicker();
}
timePicker.setValue(getItem());
setText(null);
setGraphic(timePicker);
// Wrapped this in a Platform#runLater call because otherwise
// I couldn't get this to work properly. Despite this, there are
// times where this still seems buggy.
Platform.runLater(() -> {
timePicker.requestFocus();
timePicker.getEditor().selectAll();
});
}
}
#Override
public void cancelEdit() {
super.cancelEdit();
setText(formatItem(getItem()));
setGraphic(null);
}
private void createTimePicker() {
timePicker = new JFXTimePicker();
timePicker.setConverter(new LocalTimeStringConverter(getFormatter(), null));
formatter.addListener((observable, oldValue, newValue) ->
timePicker.setConverter(new LocalTimeStringConverter(newValue, null)));
timePicker.getEditor().setOnKeyReleased(event -> {
if (event.getCode() == KeyCode.ENTER) {
commitEdit(timePicker.getValue());
event.consume();
} else if (event.getCode() == KeyCode.ESCAPE) {
cancelEdit();
event.consume();
}
});
timePicker.focusedProperty().addListener((observable, oldValue, newValue) -> {
if (!newValue) {
cancelEdit();
}
});
}
}
Here, if the ESCAPE key is released or if the JFXTimePicker loses focus then the edit is cancelled. It appears that interacting with the clock does not cause the JFXTimePicker to lose focus (at least when I tried it).
If you want to commit the edit you have to press the ENTER key. This works (again, at least when I tried it) even if the clock is currently showing.
This doesn't commit the edit automatically when the clock closes but you should be able to add that behavior if desired. Since JFXTimePicker extends from ComboBoxBase it has properties such as onHiding and onHidden.
Note: If, after typing the time manually, you attempt to commit and the DateTimeFormatter is unable to parse the String it simply reverts to the old value. There is no indication of any error other than the fact the value hasn't changed. This seems to be behavior caused by the JFXTimePicker, however.
You also don't need to try and handle committing the value yourself, either. The TableColumn already attempts to set the new value on the underlying property by default. This is mentioned in the Javadoc of TableView (under the "Editing" header, emphasis mine):
When you call Cell.commitEdit(Object) an event is fired to the
TableView, which you can observe by adding an EventHandler via
TableColumn.setOnEditCommit(javafx.event.EventHandler). Similarly, you
can also observe edit events for edit start and edit cancel.
By default the TableColumn edit commit handler is non-null, with a
default handler that attempts to overwrite the property value for the
item in the currently-being-edited row. It is able to do this as the
Cell.commitEdit(Object) method is passed in the new value, and this is
passed along to the edit commit handler via the CellEditEvent that is
fired. It is simply a matter of calling
TableColumn.CellEditEvent.getNewValue() to retrieve this value.
If you do end up using your own EventHandler in setOnEditCommit then you need to implement the behavior yourself:
It is very important to note that if you call
TableColumn.setOnEditCommit(javafx.event.EventHandler) with your own
EventHandler, then you will be removing the default handler. Unless
you then handle the writeback to the property (or the relevant data
source), nothing will happen. You can work around this by using the
TableColumnBase.addEventHandler(javafx.event.EventType,
javafx.event.EventHandler) method to add a
TableColumn.editCommitEvent() EventType with your desired EventHandler
as the second argument. Using this method, you will not replace the
default implementation, but you will be notified when an edit commit
has occurred.

CellFactory that calculates value in the background

I would like to calculate a table's cell content asynchronously in the background. I came up with the following solution:
public abstract class AsynchronousCellFactory<E, T> implements Callback<TableColumn<E, T>, TableCell<E, T>> {
#Override
public TableCell<E, T> call(final TableColumn<E, T> param) {
final TableCell<E, T> cell = new TableCell<E, T>() {
private Service<T> service;
#Override
public void updateItem(final T item, final boolean empty) {
super.updateItem(item, empty);
if (service != null) {
service.cancel();
}
if (empty || this.getTableRow() == null || this.getTableRow().getItem() == null) {
setText(null);
} else {
setText("Calculating..");
final E rowDataItem = (E) this.getTableRow().getItem();
service = new Service<T>() {
#Override
protected Task<T> createTask() {
return getTask(rowDataItem);
}
};
service.setOnSucceeded(e -> {
setText(e.getSource().getValue().toString());
});
service.setOnFailed(e -> {
final Throwable t = e.getSource().getException();
setText(t.getLocalizedMessage());
});
service.start();
}
}
};
return cell;
}
protected abstract Task<T> getTask(E rowDataItem);
}
I use this factory for almost all of my columns to calculate different values.
But this does not work very well.
I frequently see cell contents saying Calculating. When I double click on that cell, which triggers a repaint apparently, the correct value appears.
Furthermore, even worse, I see sometimes that cell content is "switched" between columns: stringA, that should be in columnA, is in comlumnB, for example. Again, a double click on that cell fixes that.
I know that those factories are reused by the table columns to calculate cell content for different items, I assume the error is connected to that fact.
EDIT: When I move the service field to the TableCell, the Calculating error seems to be fixed, nevertheless, the switched-values problem persists.
Find a MWE here. It is actually not a working example, since it does not reproduce the problem. I guess the cell factory is working OK, and I am doing something wrong somewhere else.

JavaFX connected comboboxes

I could really use some help.
I am creating application that has two connected comboboxes in a way that if i select productCode in first in second one should be selected productName.
Both combobox textfields are filtrable for search purposes.
I have set setCellFactories like this (for purpose of dropdown list rendering).
cbSifra.setCellFactory((comboBox) -> new ListCell<Product>() {
#Override
protected void updateItem(Product product, boolean empty) {
super.updateItem(product, empty);
if (product == null || empty) {
setText(null);
} else {
setText(product.getProductCode());
}
}
});
cbNaziv.setCellFactory((comboBox) -> new ListCell<Product>() {
#Override
protected void updateItem(Product product, boolean empty) {
super.updateItem(product, empty);
if (product == null || empty) {
setText(null);
} else {
setText(product.getProductName());
}
}
});
Both comboboxes implement converters to show data into combobox when selected.
cbNaziv.setConverter(new StringConverter<Product>() {
#Override
public String toString(Product product) {
if (product == null) {
return null;
} else {
return product.productNameProperty().get();
}
}
#Override
public Product fromString(String productString)
{
return cbNaziv.getItems().stream().filter(item->productString.equals(item.getProductName())).findFirst().orElse(null);
}
});
cbSifra.setConverter(new StringConverter<Product>() {
#Override
public String toString(Product product) {
if (product == null) {
return null;
} else {
return product.productCodeProperty().get();
}
}
#Override
public Product fromString(String productString)
{
return cbSifra.getItems().stream().filter(item ->productString.equals(item.getProductCode())).findAny().orElse(null);
}
});
Filtering of dropdown list is done using Listener on textProperty() like this:
cbNaziv.getEditor().textProperty().addListener((obs, oldValue, newValue) -> {
cbNaziv.show();
final TextField editor = cbNaziv.getEditor();
final Product selected = cbNaziv.getSelectionModel().getSelectedItem();
/*
This needs run on the GUI thread to avoid the error described
here: https://bugs.openjdk.java.net/browse/JDK-8081700.
*/
Platform.runLater(() -> {
/*
If the no item in the list is selected or the selected item
isn't equal to the current input, we refilter the list.
*/
if (selected == null || !selected.equals(editor.getText())) {
filteredProductList.setPredicate(item -> {
// We return true for any items that contains the
// same letters as the input. We use toUpperCase to
// avoid case sensitivity.
if (item.getProductName().toUpperCase().contains(newValue.toUpperCase())) {
return true;
} else {
return false;
}
});
}
});
});
cbSifra.getEditor().textProperty().addListener((obs, oldValue, newValue) -> {
cbSifra.show(); // Is used to open dropdown list as i start typing
final TextField editor = cbSifra.getEditor();
final Product selected = cbSifra.getSelectionModel().getSelectedItem();
/*
This needs run on the GUI thread to avoid the error described
here: https://bugs.openjdk.java.net/browse/JDK-8081700.
*/
Platform.runLater(() -> {
/*
If the no item in the list is selected or the selected item
isn't equal to the current input, we refilter the list.
*/
if (selected == null || !selected.equals(editor.getText())) {
filteredProductList.setPredicate(item -> {
// We return true for any items that contains the
// same letters as the input. We use toUpperCase to
// avoid case sensitivity.
if (item.getProductCode().toUpperCase().contains(newValue.toUpperCase())) {
return true;
} else {
return false;
}
});
}
});
});
I have valueProperty Listeners to check if value is selected and to fill some textFields to their values or set them to null.
cbSifra.valueProperty().addListener(new ChangeListener<Product>() {
#Override
public void changed(ObservableValue<? extends Product> observable, Product oldValue, Product newValue) {
if (cbSifra.getValue() == null || cbSifra.getValue().getProductName().isEmpty())
{
cbNaziv.getSelectionModel().clearSelection();
tfMpCijena.setText(null);
tfPopust.setText(null);
} else {
cbNaziv.setValue(cbSifra.getValue());
cbSifra.setValue(cbNaziv.getValue());
cbNaziv.hide();
tfMpCijena.setText(cbSifra.getValue().getProductRetailPrice().toString());
tfPopust.setText("0");
}
}
});
cbNaziv.valueProperty().addListener(new ChangeListener<Product>() {
#Override
public void changed(ObservableValue<? extends Product> observable, Product oldValue, Product newValue) {
if (cbNaziv.getValue() == null || cbNaziv.getValue().getProductName().isEmpty())
{
cbSifra.getSelectionModel().clearSelection();
tfMpCijena.setText(null);
tfPopust.setText(null);
} else {
cbSifra.setValue(cbNaziv.getValue());
cbSifra.hide();
tfMpCijena.setText(cbNaziv.getValue().getProductRetailPrice().toString());
tfPopust.setText("0");
}
}
});
The problems are :
when i start typing something into any combobox it filters ok and
when i select item from dropdown list it fills second combobox but
first combobox editor gets focus again and displays dropdown
again
when i delete entry from combobox it deletes ok but the other
combobox value remains (it isnt deleted)
If you can help me i would really appreciate it.
Thanks in advance.
Assuming that both ComboBoxes are of type Product, you can use bidirectional binding to ensure that the values for both ComboBoxes always point to the same product.
cbNaziv.valueProperty().bindBidirectional(cbSifra.valueProperty());
Using this should allow you to remove your change listeners and will hopefully fix some of the issues you were having.

Resources