Reputation: 1059
I have several ComboBox
that are used to pick a date and time for an appointment scheduler application, but when I customize the CellFactory
it only properly disables the correct items the very first time you click and expand. Basically it should disable selections that allow you to select a date and time that is prior to LocalDateTime.now()
. Here is a video showing the erratic behavior: https://www.youtube.com/watch?v=niGoIvVx9Y0
Here is the code for my cell factories:
startHour.setCellFactory((final ListView<String> param) -> {
return new ComboBoxListCell<String>() {
@Override
public void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if(empty || item == null) {
setText(null);
setGraphic(null);
} else {
LocalDateTime selected = createDateTime(startDate.getValue(), item,
startMinute.getValue(), startMeridian.getValue());
if(selected.isBefore(LocalDateTime.now())) {
System.out.print(item + ", ");
setDisable(true);
setStyle(COMBOCELL_DISABLED_COLOR);
}
}
}
};
});
startMinute.setCellFactory((final ListView<String> listView) -> {
return new ComboBoxListCell<String>() {
@Override
public void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if(empty || item == null) {
setText(null);
setGraphic(null);
} else {
LocalDateTime selected = createDateTime(startDate.getValue(), startHour.getValue(),
item, startMeridian.getValue());
if(selected.isBefore(LocalDateTime.now())) {
setDisable(true);
setStyle(COMBOCELL_DISABLED_COLOR);
}
}
}
};
});
startMeridian.setCellFactory((final ListView<String> listView) -> {
return new ComboBoxListCell<String>() {
@Override public void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if(empty || item == null) {
setText(null);
setGraphic(null);
} else {
LocalDateTime selected = createDateTime(startDate.getValue(), startHour.getValue(),
startMinute.getValue(), item);
if(selected.isBefore(LocalDateTime.now())) {
setDisable(true);
setStyle(COMBOCELL_DISABLED_COLOR);
}
}
}
};
});
the selected LocalDateTime
is calculated using this method:
private LocalDateTime createDateTime(LocalDate date, String hour, String minute, String meridian) {
String dateTimeStr = date + " " + hour + " " + minute + " " + meridian;
return LocalDateTime.parse(dateTimeStr, DateTimeFormatter.ofPattern("yyyy-MM-dd h m a"));
}
in startHour#setCellFactory
I inserted a println
statement showing the selected item converted into a LocalDateTime
if it occured before LocalDateTime.now()
, and this is the output I got:
1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 2, 3, 4, 5, 6, 7, 8, 9, 12, 1, 1, 2, 3, 4, 5, 6, 7, 8, 9, 12, 12, 12, 1, 9, 12, 1, 9, 12, 2, 1, 7, 9,
The current time was 9:48AM and the selected time was 10:00AM, so the intended condition appears to be met. However, as the video shows, the behavior is very erratic AFTER expanding the ComboBox
for the first time. I have similar cell factories for the DatePicker
nodes and they work just fine.
Am I misunderstanding how ComboBox#setCellFactory
is intended to be used?
Upvotes: 0
Views: 670
Reputation: 209330
For the startHour
, you need the disabled state of the cell to (potentially) update when any of the cell's current item, the selected start date, the selected start minute, or the selected meridian change. You can do this with a binding.
(Note that you are using the wrong cell class. A ComboBoxListCell
is for displaying an editable cell in a list that uses a combo box as the editor. You just need a plain ListCell
here.)
startHour.setCellFactory((final ListView<String> param) -> {
ListCell<String> cell = new ListCell<String>() {
@Override
public void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
setText(item); // works in empty case as item will be null, as desired
}
};
cell.disableProperty().bind(Bindings.createBooleanBinding(
() -> {
if (cell.getItem() == null) return false ;
LocalDateTime selected = createDateTime(startDate.getValue(), cell.getItem(),
startMinute.getValue(), startMeridian.getValue());
return selected.isBefore(LocalDateTime.now());
},
cell.itemProperty(),
startMinute.valueProperty(),
startMeridian.valueProperty(),
startDate.valueProperty()
));
return cell ;
});
The other cell factories will be similar.
For the style, I strongly recommend you use an external style sheet, with a rule such as:
.combo-box .list-cell:disabled {
/* style for disabled cell here */
}
This keeps the code simpler, and means there is only one place to define the style for all the relevant disabled combo box cells.
Note that once a cell has been displayed that represents a time after the current time (so it is enabled), if it is displayed sufficiently later that it is no longer after the current time, then it won't be automatically disabled. (So, for example, if a cell representing 1pm on April 24th 2017 is first displayed at 12:55pm that day, it will be enabled. If it is displayed again 10 minutes later with no other UI changes that cause updateItem()
to be called, it should be disabled, but will not be.) A complete solution would have the binding also observe the "current time" as it changes, for which you could do:
ObjectProperty<LocalDateTime> clock = new SimpleObjectProperty<>(LocalDateTime.now());
Timeline clockwork = new Timeline(new KeyFrame(Duration.seconds(1),
e -> clock.set(LocalDateTime.now())));
clockwork.setCycleCount(Animation.INDEFINITE);
clockwork.play();
and then add clock
to the list of dependent values in the binding:
cell.disableProperty().bind(Bindings.createBooleanBinding(
() -> {
if (cell.getItem() == null) return false ;
LocalDateTime selected = createDateTime(startDate.getValue(), cell.getItem(),
startMinute.getValue(), startMeridian.getValue());
return selected.isBefore(clock.get());
},
cell.itemProperty(),
startMinute.valueProperty(),
startMeridian.valueProperty(),
startDate.valueProperty(),
clock
));
Upvotes: 1