JavaFX TableView with Custom Cell Datepicker "OnEditCommit" is not invoked - datepicker

Hi everyone !!
I'm working with JavaFX and the JDK 8.
I got a TableView filled by a database with the use of a JavaFX POJO Class "Inputs". I've implemented TexField cells, ComboBoxes cells and Checkbox cells succesfully.
But I am not able to make it working with DatePicker Control: the event on the "setOnEditComit" is never invoked...
Hre is my code:
Declarations for TableColumn:
#FXML
private TableColumn<Inputs,LocalDate> colDate = new TableColumn<Inputs,LocalDate>();
[...]
colDate.setCellValueFactory(
new PropertyValueFactory<Inputs,LocalDate>("localdate"));
Callback<TableColumn<Inputs, LocalDate>, TableCell<Inputs, LocalDate>> dateCellFactory =
new Callback<TableColumn<Inputs, LocalDate>, TableCell<Inputs, LocalDate>>() {
public TableCell call(TableColumn p) {
return new EditingDatePickerCell();
}
};
[...]
colDate.setOnEditCommit(
new EventHandler<CellEditEvent<Inputs, LocalDate>>() {
#Override
public void handle(CellEditEvent<Inputs, LocalDate> event) {
System.out.println("EVENT!!!!");
((Inputs) event.getTableView().getItems().get(
event.getTablePosition().getRow())
).setDatePicker(event.getNewValue());
}
}
);
With these JavaFX POJO Objects declarations:
public ObjectProperty<LocalDate> localdate;
[...]
this.localdate = new SimpleObjectProperty<LocalDate>(localdate);
[...]
public LocalDate getDatePicker() {
return localdate.getValue();
}
[...]
public void setDatePicker(LocalDate value) {
System.out.println("EVENT!!!!");
localdate.setValue(value);
}
[...]
public ObjectProperty<LocalDate> localdateProperty() {
return localdate;
}
EditingDatePickerCell class:
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import javafx.application.Platform;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Pos;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.DatePicker;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import com.desktop.newapp.utils.Inputs;
public class EditingDatePickerCell <Inputs, LocalDate> extends TableCell<Inputs, LocalDate> {
#Override
public void startEdit() {
if (!isEmpty()) {
super.startEdit();
createDatePicker();
setText(null);
setGraphic(datePicker);
datePicker.requestFocus();
}
}
#Override
public void cancelEdit() {
super.cancelEdit();
datePicker.setValue((java.time.LocalDate) getItem());
setGraphic(null);
setContentDisplay(ContentDisplay.TEXT_ONLY);
}
private DatePicker datePicker;
public EditingDatePickerCell() {
if (datePicker == null) {
createDatePicker();
}
setGraphic(datePicker);
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
Platform.runLater(new Runnable() {
#Override
public void run() {
datePicker.requestFocus();
}
});
}
#Override
public void updateItem(LocalDate item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setText(null);
setGraphic(null);
} else {
if (datePicker != null && item != null) {
datePicker.setValue((java.time.LocalDate) getLocalDate());
commitEdit(getLocalDate());
}
setGraphic(datePicker);
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
}
}
private void createDatePicker() {
datePicker = new DatePicker();
datePicker.setMinWidth(this.getWidth() - this.getGraphicTextGap() * 2);
setGraphic(datePicker);
datePicker.addEventFilter(KeyEvent.KEY_PRESSED,new EventHandler<KeyEvent>() {
#Override
public void handle(KeyEvent t) {
if (t.getCode() == KeyCode.ENTER) {
commitEdit(getLocalDate());
} else if (t.getCode() == KeyCode.ESCAPE) {
cancelEdit();
} else if (t.getCode() == KeyCode.TAB) {
commitEdit(getLocalDate());;
}
}
});
setAlignment(Pos.CENTER);
}
private LocalDate getLocalDate() {
return getItem();
///return datePicker.getValue() != null ? datePicker.getValue() : getItem();
}
}
I've been Googling a lot and I didn't find any correct implementation of the Java8 DatePicker Editing capabilities in a JavaFX TableView. Any help would be very appreciate !!
Without Stackoverflow I'll never could done the quarter of the whole job so long life to Stackoverflow !!
Regards.

You need to put the table in editing mode (for the current row) when the user interacts with the picker. That can be achieved with an onShowingHandler like this (example is also with a color picker):
this.colorPicker.setOnShowing(event -> {
final TableView<T> tableView = getTableView();
tableView.getSelectionModel().select(getTableRow().getIndex());
tableView.edit(tableView.getSelectionModel().getSelectedIndex(), column);
});
then watch the valueProperty of the picker like this:
this.colorPicker.valueProperty().addListener((observable, oldValue, newValue) -> {
if(isEditing()) {
commitEdit(newValue);
}
});
For more explanation have a look at my blog post: Custom editor components in JavaFX TableCells.
Full Class:
public class ColorTableCell<T> extends TableCell<T, Color> {
private final ColorPicker colorPicker;
public ColorTableCell(TableColumn<T, Color> column) {
this.colorPicker = new ColorPicker();
this.colorPicker.editableProperty().bind(column.editableProperty());
this.colorPicker.disableProperty().bind(column.editableProperty().not());
this.colorPicker.setOnShowing(event -> {
final TableView<T> tableView = getTableView();
tableView.getSelectionModel().select(getTableRow().getIndex());
tableView.edit(tableView.getSelectionModel().getSelectedIndex(), column);
});
this.colorPicker.valueProperty().addListener((observable, oldValue, newValue) -> {
if(isEditing()) {
commitEdit(newValue);
}
});
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
}
#Override
protected void updateItem(Color item, boolean empty) {
super.updateItem(item, empty);
setText(null);
if(empty) {
setGraphic(null);
} else {
this.colorPicker.setValue(item);
this.setGraphic(this.colorPicker);
}
}
}

I was having the same issues but i ended up solving it with this post. JavaFX8 – Render a DatePicker Cell in a TableView.
The outcome is illustrated in the image below.

Your Model object is a bit strange. The setDatePicker and getDatePicker methods should be called setLocaldate and getLocaldate, to be consistent with the localdateProperty() method and (possibly importantly) the parameter passed to the PropertyValueFactory.
I don't think that would cause the problem you observe, though, and I can't see what else is wrong.
This example works for me.
Have a look and see if you can see what I did that's different...

here is a datapicker cell edit when pressing tab key and go to the next cell
datePicker.setOnKeyPressed /EventHandler doesnot work with tab
datePicker.EventFilter get called twice
to work arround this you need to add a listner to tableview cell and get date text as string and cast it to localdate
addEventFilter to TableCell deduction object is a sample
=> addEventFilter(KeyEvent.KEY_PRESSED, new EventHandler() {...}
public class DatePickerCell extends TableCell<Deduction, LocalDate> {
private DatePicker datePicker;
private TableColumn column;
// private TableCell cell;
public DatePickerCell(TableColumn column) { //
super();
this.column = column;
this.datePicker = new DatePicker(getDate());
}
#Override
public void startEdit() {
if (!isEmpty()) {
super.startEdit();
createDatePicker();
setText(null);
setGraphic(datePicker);
Platform.runLater(new Runnable() {
#Override
public void run() {
datePicker.requestFocus();
}
});
//Platform.runLater(datePicker::requestFocus);
}
}
#Override
public void updateItem(LocalDate item, boolean empty) {
super.updateItem(item, empty);
// SimpleDateFormat smp = new SimpleDateFormat("dd/MM/yyyy");
/* if (null == this.datePicker) {
System.out.println("datePicker is NULL");
}*/ //TO be reviewed
if (empty) {
setText(null);
setGraphic(null);
} else {
if (isEditing()) {
if (datePicker != null) {
datePicker.setValue(getItem());
}
setText(null);
setGraphic(datePicker);
} else {
setText(getItem().toString());
setGraphic(null);
}
}
}
private LocalDate getDate() {
return getItem() == null ? LocalDate.now() : getItem();
}
private void createDatePicker() {
this.datePicker = new DatePicker(getDate());
datePicker.setMinWidth(this.getWidth() - this.getGraphicTextGap() * 2);
this.datePicker.editableProperty().bind(column.editableProperty());
this.datePicker.disableProperty().bind(column.editableProperty().not());
this.datePicker.setOnShowing(event -> {
final TableView tableView = getTableView();
tableView.getSelectionModel().select(getTableRow().getIndex());
tableView.edit(tableView.getSelectionModel().getSelectedIndex(), column);
});
datePicker.setOnAction((e) -> {
commitEdit(datePicker.getValue());
TableColumn nextColumn = getNextColumn(true);
if (nextColumn != null) {
getTableView().edit(getTableRow().getIndex(),
nextColumn);
}
});
datePicker.focusedProperty().addListener(
new ChangeListener<Boolean>() {
#Override
public void changed(
ObservableValue<? extends Boolean> arg0,
Boolean arg1, Boolean arg2) {
if (!arg2) {
commitEdit(datePicker.getValue());
}
}
});
datePicker.setOnKeyPressed(new EventHandler<KeyEvent>() {
#Override
public void handle(KeyEvent t) {
// System.out.println("setOnKeyPressed datapicker get value ...." + t.getCode());
/* if (t.getCode() == KeyCode.ENTER) {
commitEdit(datePicker.getValue());
System.out.println("Enter datapicker get value " + datePicker.getValue());
} else */if (t.getCode() == KeyCode.ESCAPE) {
cancelEdit();
} else if (t.getCode() == KeyCode.ENTER){
commitEdit(datePicker.getValue());
// System.out.println("Tab datapicker get value " + datePicker.getValue());
TableColumn nextColumn = getNextColumn(!t.isShiftDown());
// System.out.println("nextColumn datapicker get value " + nextColumn.getId());
if (nextColumn != null) {
getTableView().edit(getTableRow().getIndex(),
nextColumn);
}
}
}
});
addEventFilter(KeyEvent.KEY_PRESSED, new EventHandler<KeyEvent>() {
#Override
public void handle(KeyEvent event) {
if (event.getCode() == KeyCode.TAB) {
commitEdit(LocalDate.parse(datePicker.getEditor().getText(), DateTimeFormatter.ofPattern("dd/MM/yyyy")));
TableColumn nextColumn = getNextColumn(true);
if (nextColumn != null) {
getTableView().edit(getTableRow().getIndex(),
nextColumn);
}
}
}
});
//
// // setAlignment(Pos.CENTER);
}
#Override
public void cancelEdit() {
super.cancelEdit();
setText(getItem().toString());
setGraphic(null);
}
public DatePicker getDatePicker() {
return datePicker;
}
public void setDatePicker(DatePicker datePicker) {
this.datePicker = datePicker;
}
private String getString() {
return getItem() == null ? "" : getItem().toString();
}
private TableColumn<Deduction, ?> getNextColumn(boolean forward) {
List<TableColumn<Deduction, ?>> columns = new ArrayList<>();
for (TableColumn<Deduction, ?> column : getTableView().getColumns()) {
columns.addAll(getLeaves(column));
}
// There is no other column that supports editing.
if (columns.size() < 2) {
return null;
}
int currentIndex = columns.indexOf(getTableColumn());
int nextIndex = currentIndex;
if (forward) {
nextIndex++;
if (nextIndex > columns.size() - 1) {
nextIndex = 0;
}
} else {
nextIndex--;
if (nextIndex < 0) {
nextIndex = columns.size() - 1;
}
}
return columns.get(nextIndex);
}
private List<TableColumn<Deduction, ?>> getLeaves(
TableColumn<Deduction, ?> root) {
List<TableColumn<Deduction, ?>> columns = new ArrayList<>();
if (root.getColumns().isEmpty()) {
// We only want the leaves that are editable.
if (root.isEditable()) {
columns.add(root);
}
return columns;
} else {
for (TableColumn<Deduction, ?> column : root.getColumns()) {
columns.addAll(getLeaves(column));
}
return columns;
}
}
}
in your class with the tableview
Callback cellFactoryDate
= new Callback<TableColumn, TableCell>() {
public TableCell call(TableColumn p) {
return new DatePickerCell(p);//DatePickerTest(p) //DatePickerTest2 //EditingCellDate
}
};
dateColumn.setCellValueFactory(new PropertyValueFactory<>("date"));
// dateColumn.setCellFactory(cellFactoryTest);//cellFactoryDate
dateColumn.setCellFactory(cellFactoryDate);
dateColumn.setOnEditCommit(
new EventHandler<TableColumn.CellEditEvent<Deduction, LocalDate>>() {
#Override
public void handle(TableColumn.CellEditEvent<Deduction, LocalDate> t) {
((Deduction) t.getTableView().getItems().get(
t.getTablePosition().getRow())).setDate(t.getNewValue());
}
}
);

Here is a solution acceptable for all custom controls; this example use a ColorPicker control:
So finnaly I deal it like that: on the events of the controls I get back the POJO objects in use by the "((Inputs)getTableView().getItems().get(getTableRow().getIndex()" and I update similary like is it done in the OnEditCommit method...
So for me it's look like this (update the color):
((Inputs) getTableView().getItems().get(
getTableRow().getIndex())
).setColor(cp.getValue());
Here is example with ColorPickerCell
:
public class ColorPickerTableCell<Inputs> extends TableCell<Inputs, Color>{
private ColorPicker cp;
public ColorPickerTableCell(){
cp = new ColorPicker();
cp.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent event) {
commitEdit(cp.getValue());
updateItem(cp.getValue(), isEmpty());
((Inputs) getTableView().getItems().get(
getTableRow().getIndex())
).setColor(cp.getValue());
}
});
setGraphic(cp);
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
setEditable(true);
}
#Override
protected void updateItem(Color item, boolean empty) {
super.updateItem(item, empty);
cp.setVisible(!empty);
this.setItem(item);
cp.setValue(item);
}
}
With this simple JavaFX's POJO:
public ObjectProperty<Color> color = new SimpleObjectProperty<Color>();
this.color = new SimpleObjectProperty(color);
public ObjectProperty<Color> colorProperty() {
return color;
}
public void setColor(Color color2) {
color.set(color2);
}
I do not know if it's a good way to achive that but it worked for me... Note that the JavaFX's POJO is only accessible within an "ActionEvent" request (combobox, datepicker, colorpicker, etc..)
Regards,

Related

i want after editing of tableview cell textfield ,when i type i want auto suggestion of word

this code contain basically how to edit the textfield of tableview column ,like tableview column is in textfield format and i need to edit it and when i will type i need auto suggestion but my auto suggestion code is not working ,so can anyone suggest me or help to overcome my auto suggestion of word problem with the same procedure ,in createfield after commitedit i called learnword function as well but that is not happening.
class EditingCell extends TableCell<File, String> {
Set<String> possibleWordSet= new HashSet<>();
private AutoCompletionBinding<String> autoCompletionBinding;
public TextField textField;
//private Object possibleWordSet;
#Override
public void startEdit() {
if (!isEmpty()) {
super.startEdit();
createTextField();
setText(null);
setGraphic(textField);
System.out.println(textField.getCaretPosition() + "caret position");
}
}
#Override
public void cancelEdit() {
super.cancelEdit();
setText(getItem());
setGraphic(null);
}
#Override
public void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
System.out.println("I am in update function");
setText(null);
setGraphic(null);
} else {
if (isEditing()) {
if (textField != null) {
textField.setText(item);
}
setText(null);
setGraphic(textField);
} else {
setText(item);
setGraphic(null);
}
}
}
// Instantiates the text field.
public void createTextField() {
textField = new TextField(getItem());
System.out.println("hello i am inside the createtextfield");
textField.setMinWidth(this.getWidth() - this.getGraphicTextGap() * 2);
autoCompletionBinding = TextFields.bindAutoCompletion(textField, possibleWordSet); /tion/for auto suuges
textField.setOnKeyReleased(new EventHandler<KeyEvent>() {
#Override
public void handle(KeyEvent t) {
if (t.getCode() == KeyCode.ENTER) {
commitEdit(textField.getText());//on enter event editing of textfield
learnword(textField.getText());//for auto suggestion call learnword method.
} else if (t.getCode() == KeyCode.ESCAPE) {
cancelEdit();
}
}
});
textField.setOnMouseClicked(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
int caretPosition = textField.getCaretPosition();
GUIFXMLDocumentController fx = new GUIFXMLDocumentController();
fx.setCaretPos(caretPosition);
System.out.println(caretPosition + "caretPosition");
fx.setTextField(textField);
}
});
textField.focusedProperty().addListener(new ChangeListener<Boolean>() {
#Override
public void changed(ObservableValue<? extends Boolean> arg0, Boolean arg1, Boolean arg2) {
if (!arg2) {
System.out.println(textField.getText() + "Inside changeListener------------------");
GUIFXMLDocumentController.tv_new.getFocusModel().focusNext();
commitEdit(textField.getText());
learnword(textField.getText());
}
}
});
}
public void learnword(String text) {
possibleWordSet.add(text);
if(autoCompletionBinding!=null){
autoCompletionBinding.dispose();
}
autoCompletionBinding = TextFields.bindAutoCompletion(textField, possibleWordSet);
}
}
enter code here

ListView validate edit and prevent commit

I'm using an editable ListView containing Patterns.
The user can see and edit the regexs in the list, and I'd like to validate whether the regex is syntactically correct before committing the value (and give feedback like a red border to the user).
Is there a way to do so?
patternList.setCellFactory(TextFieldListCell.forListView(new StringConverter<Pattern>() {
#Override
public String toString(Pattern pattern) {
return pattern.toString();
}
#Override
public Pattern fromString(String string) {
try {
return Pattern.compile(string);
} catch (PatternSyntaxException e) {
return null;
}
}
}));
patternList.setOnEditCommit(e -> {
if (e.getNewValue() == null) {
// TODO pattern syntax error, prevent commit and stay in edit mode
} else {
patternList.getItems().set(e.getIndex(), e.getNewValue());
}
});
I would do this by creating a TableCell implementation. E.g.:
import java.util.function.Predicate;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.css.PseudoClass;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.TableCell;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
public class ValidatingEditingCell<S> extends TableCell<S, String> {
private final TextField textField ;
private static final PseudoClass INVALID = PseudoClass.getPseudoClass("invalid");
private BooleanProperty valid = new SimpleBooleanProperty();
public ValidatingEditingCell(Predicate<String> validator) {
this.textField = new TextField();
valid.bind(Bindings.createBooleanBinding(() -> textField.getText() != null && validator.test(textField.getText()),
textField.textProperty()));
valid.addListener((obs, wasValid, isValid) -> {
pseudoClassStateChanged(INVALID, ! isValid);
});
pseudoClassStateChanged(INVALID, ! valid.get());
textField.addEventHandler(KeyEvent.KEY_PRESSED, e -> {
if (e.getCode() == KeyCode.ENTER && valid.get()) {
commitEdit(textField.getText());
}
if (e.getCode() == KeyCode.ESCAPE) {
cancelEdit();
}
});
setGraphic(textField);
setContentDisplay(ContentDisplay.TEXT_ONLY);
}
#Override
protected void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
setText(empty ? null : item);
textField.setText(empty ? null : item);
setContentDisplay(isEditing() ? ContentDisplay.GRAPHIC_ONLY : ContentDisplay.TEXT_ONLY);
}
#Override
public void cancelEdit() {
super.cancelEdit();
setContentDisplay(ContentDisplay.TEXT_ONLY);
}
#Override
public void commitEdit(String newValue) {
super.commitEdit(newValue);
setContentDisplay(ContentDisplay.TEXT_ONLY);
}
#Override
public void startEdit() {
super.startEdit();
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
textField.selectAll();
textField.requestFocus();
}
}
This takes a predicate as an argument; the predicate returns true for valid text and false for invalid text. It sets a CSS pseudoclass on the cell, so you can use CSS to style the text field (or cell itself, if needed).
Here's a simple example which validates three different columns differently:
import java.util.function.Function;
import java.util.function.Predicate;
import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
public class ValidatingTableExample extends Application {
private static <S> TableColumn<S, String> column(String title, Function<S, StringProperty> property,
Predicate<String> validator) {
TableColumn<S, String> col = new TableColumn<>(title);
col.setCellValueFactory(cellData -> property.apply(cellData.getValue()));
col.setCellFactory(tc -> new ValidatingEditingCell<>(validator));
col.setPrefWidth(150);
return col ;
}
#Override
public void start(Stage primaryStage) {
TableView<Address> table = new TableView<>();
table.setEditable(true);
table.getColumns().add(column("City", Address::cityProperty, s -> ! s.isEmpty()));
table.getColumns().add(column("State", Address::stateProperty, s -> s.length()==2));
table.getColumns().add(column("Zip", Address::zipProperty, s -> s.matches("\\d{5}")));
Button newAddress = new Button("Add");
newAddress.setOnAction(e -> {
table.getItems().add(new Address("City", "State", "Zip"));
});
Button debug = new Button("Debug");
debug.setOnAction(e ->
table.getItems().stream()
.map(address -> String.format("%s, %s %s", address.getCity(), address.getState(), address.getZip()))
.forEach(System.out::println));
HBox buttons = new HBox(5, newAddress, debug);
buttons.setAlignment(Pos.CENTER);
buttons.setPadding(new Insets(5));
BorderPane root = new BorderPane(table, null, null, buttons, null);
Scene scene = new Scene(root, 600, 600);
scene.getStylesheets().add(getClass().getResource("validating-cell.css").toExternalForm());
primaryStage.setScene(scene);
primaryStage.show();
}
public static class Address {
private final StringProperty city = new SimpleStringProperty();
private final StringProperty state = new SimpleStringProperty();
private final StringProperty zip = new SimpleStringProperty();
public Address(String city, String state, String zip) {
setCity(city);
setState(state);
setZip(zip);
}
public final StringProperty cityProperty() {
return this.city;
}
public final String getCity() {
return this.cityProperty().get();
}
public final void setCity(final String city) {
this.cityProperty().set(city);
}
public final StringProperty stateProperty() {
return this.state;
}
public final String getState() {
return this.stateProperty().get();
}
public final void setState(final String state) {
this.stateProperty().set(state);
}
public final StringProperty zipProperty() {
return this.zip;
}
public final String getZip() {
return this.zipProperty().get();
}
public final void setZip(final String zip) {
this.zipProperty().set(zip);
}
}
public static void main(String[] args) {
launch(args);
}
}
and some sample CSS:
.table-cell:invalid .text-field {
-fx-focus-color: red ;
-fx-control-inner-background: #ffc0c0 ;
-fx-accent: red ;
}
I finally found a way, by overriding the commitEdit() method of TextFieldListCell:
patternList.setCellFactory(l -> new TextFieldListCell<Pattern>(new StringConverter<Pattern>() {
#Override
public String toString(Pattern pattern) {
return pattern.toString();
}
#Override
public Pattern fromString(String string) {
try {
return Pattern.compile(string);
} catch (PatternSyntaxException e) {
return null;
}
}
}) {
#Override
public void commitEdit(Pattern pattern) {
if (!isEditing()) return;
PseudoClass errorClass = PseudoClass.getPseudoClass("error");
pseudoClassStateChanged(errorClass, pattern == null);
if (pattern != null) {
super.commitEdit(pattern);
}
}
});
patternList.setOnEditCommit(e -> patternList.getItems().set(e.getIndex(), e.getNewValue()));

Selecting a period or a date using ONE JavaFX 8 DatePicker

On the application I am currently working, it is necessary to select a single date or a period from the same JavaFX 8 DatePicker.
The preferred way of doing this would be as follows:
Selecting a single date - same as default behaviour of the DatePicker.
Selecting a period - select start/end date by holding down the mouse button and drag to the desired end/start date. When the mouse button is released you have defined your period. The fact that you cannot select dates other than those displayed is acceptable.
Editing should work for both single date (ex 24.12.2014) and period ( ex: 24.12.2014 - 27.12.2014)
A possible rendering of the selected period (minus the content of the text editor) above would look like this:
Where orange indicates current date, blue indicates selected period. The picture is from a prototype I made, but where the period is selected by using 2 DatePickers rather than one.
I had a look at the sourcecode for
com.sun.javafx.scene.control.skin.DatePickerContent
which has a
protected List<DateCell> dayCells = new ArrayList<DateCell>();
in order to find a way of detecting when the mouse selected a date end when the mouse was released (or maybe detecting a drag).
However I am not quite sure how to go about it. Any suggestions?
I am attaching the simple prototype code I have made so far (that makes use of 2 rather than the desired 1 datepicker).
import java.time.LocalDate;
import javafx.beans.property.SimpleObjectProperty;
public interface PeriodController {
/**
* #return Today.
*/
LocalDate currentDate();
/**
* #return Selected from date.
*/
SimpleObjectProperty<LocalDate> fromDateProperty();
/**
* #return Selected to date.
*/
SimpleObjectProperty<LocalDate> toDateProperty();
}
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import javafx.util.StringConverter;
public class DateConverter extends StringConverter<LocalDate> {
private DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("dd.MM.yyyy"); // TODO i18n
#Override
public String toString(LocalDate date) {
if (date != null) {
return dateFormatter.format(date);
} else {
return "";
}
}
#Override
public LocalDate fromString(String string) {
if (string != null && !string.isEmpty()) {
return LocalDate.parse(string, dateFormatter);
} else {
return null;
}
}
}
import static java.lang.System.out;
import java.time.LocalDate;
import java.util.Locale;
import javafx.application.Application;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.geometry.HPos;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class PeriodMain extends Application {
private Stage stage;
public static void main(String[] args) {
Locale.setDefault(new Locale("no", "NO"));
launch(args);
}
#Override
public void start(Stage stage) {
this.stage = stage;
stage.setTitle("Period prototype ");
initUI();
stage.getScene().getStylesheets().add(getClass().getResource("/period-picker.css").toExternalForm());
stage.show();
}
private void initUI() {
VBox vbox = new VBox(20);
vbox.setStyle("-fx-padding: 10;");
Scene scene = new Scene(vbox, 400, 200);
stage.setScene(scene);
final PeriodPickerPrototype periodPickerPrototype = new PeriodPickerPrototype(new PeriodController() {
SimpleObjectProperty<LocalDate> fromDate = new SimpleObjectProperty<>();
SimpleObjectProperty<LocalDate> toDate = new SimpleObjectProperty<>();
{
final ChangeListener<LocalDate> dateListener = (observable, oldValue, newValue) -> {
if (fromDate.getValue() != null && toDate.getValue() != null) {
out.println("Selected period " + fromDate.getValue() + " - " + toDate.getValue());
}
};
fromDate.addListener(dateListener);
toDate.addListener(dateListener);
}
#Override public LocalDate currentDate() {
return LocalDate.now();
}
#Override public SimpleObjectProperty<LocalDate> fromDateProperty() {
return fromDate;
}
#Override public SimpleObjectProperty<LocalDate> toDateProperty() {
return toDate;
}
});
GridPane gridPane = new GridPane();
gridPane.setHgap(10);
gridPane.setVgap(10);
Label checkInlabel = new Label("Check-In Date:");
GridPane.setHalignment(checkInlabel, HPos.LEFT);
gridPane.add(periodPickerPrototype, 0, 1);
vbox.getChildren().add(gridPane);
}
}
import java.time.LocalDate;
import javafx.beans.value.ChangeListener;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.control.DateCell;
import javafx.scene.control.DatePicker;
import javafx.scene.control.Label;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.GridPane;
import javafx.util.Callback;
import javafx.util.StringConverter;
/**
* Selecting a single date or a period - only a prototype.
* As long as you have made an active choice on the {#code toDate}, the {#code fromDate} and {#code toDate} will have the same date.
*/
public class PeriodPickerPrototype extends GridPane {
private static final String CSS_CALENDAR_BEFORE = "calendar-before";
private static final String CSS_CALENDAR_BETWEEN = "calendar-between";
private static final String CSS_CALENDAR_TODAY = "calendar-today";
private static final boolean DISPLAY_WEEK_NUMBER = true;
private Label fromLabel;
private Label toLabel;
private DatePicker fromDate;
private DatePicker toDate;
private StringConverter<LocalDate> converter;
private PeriodController controller;
private ChangeListener<LocalDate> fromDateListener;
private ChangeListener<LocalDate> toDateListener;
private Callback<DatePicker, DateCell> toDateCellFactory;
private Callback<DatePicker, DateCell> fromDateCellFactory;
private Tooltip todayTooltip;
private boolean toDateIsActivlyChosenbyUser;
public PeriodPickerPrototype(final PeriodController periodController)
{
this.controller = periodController;
createComponents();
makeLayout();
createHandlers();
bindAndRegisterHandlers();
i18n();
initComponent();
}
public void createComponents() {
fromLabel = new Label();
toLabel = new Label();
fromDate = new DatePicker();
toDate = new DatePicker();
todayTooltip = new Tooltip();
}
public void createHandlers() {
fromDate.setOnAction(event -> {
if ((!toDateIsActivlyChosenbyUser) || fromDate.getValue().isAfter(toDate.getValue())) {
setDateWithoutFiringEvent(fromDate.getValue(), toDate);
toDateIsActivlyChosenbyUser = false;
}
});
toDate.setOnAction(event -> toDateIsActivlyChosenbyUser = true);
fromDateCellFactory = new Callback<DatePicker, DateCell>() {
#Override public DateCell call(final DatePicker datePicker) {
return new DateCell() {
#Override
public void updateItem(LocalDate item, boolean empty) {
super.updateItem(item, empty);
getStyleClass().removeAll(CSS_CALENDAR_TODAY, CSS_CALENDAR_BEFORE, CSS_CALENDAR_BETWEEN);
if ((item.isBefore(toDate.getValue()) || item.isEqual(toDate.getValue())) && item.isAfter(fromDate.getValue())) {
getStyleClass().add(CSS_CALENDAR_BETWEEN);
}
if (item.isEqual(controller.currentDate())) {
getStyleClass().add(CSS_CALENDAR_TODAY);
setTooltip(todayTooltip);
} else {
setTooltip(null);
}
}
};
}
};
toDateCellFactory =
new Callback<DatePicker, DateCell>() {
#Override
public DateCell call(final DatePicker datePicker) {
return new DateCell() {
#Override
public void updateItem(LocalDate item, boolean empty) {
super.updateItem(item, empty);
setDisable(item.isBefore(fromDate.getValue()));
getStyleClass().removeAll(CSS_CALENDAR_TODAY, CSS_CALENDAR_BEFORE, CSS_CALENDAR_BETWEEN);
if (item.isBefore(fromDate.getValue())) {
getStyleClass().add(CSS_CALENDAR_BEFORE);
} else if (item.isBefore(toDate.getValue()) || item.isEqual(toDate.getValue())) {
getStyleClass().add(CSS_CALENDAR_BETWEEN);
}
if (item.isEqual(controller.currentDate())) {
getStyleClass().add(CSS_CALENDAR_TODAY);
setTooltip(todayTooltip);
} else {
setTooltip(null);
}
}
};
}
};
converter = new DateConverter();
fromDateListener = (observableValue, oldValue, newValue) -> {
if (newValue == null) {
// Restting old value and cancel..
setDateWithoutFiringEvent(oldValue, fromDate);
return;
}
controller.fromDateProperty().set(newValue);
};
toDateListener = (observableValue, oldValue, newValue) -> {
if (newValue == null) {
// Restting old value and cancel..
setDateWithoutFiringEvent(oldValue, toDate);
return;
}
controller.toDateProperty().set(newValue);
};
}
/**
* Changes the date on {#code datePicker} without fire {#code onAction} event.
*/
private void setDateWithoutFiringEvent(LocalDate newDate, DatePicker datePicker) {
final EventHandler<ActionEvent> onAction = datePicker.getOnAction();
datePicker.setOnAction(null);
datePicker.setValue(newDate);
datePicker.setOnAction(onAction);
}
public void bindAndRegisterHandlers() {
toDate.setDayCellFactory(toDateCellFactory);
fromDate.setDayCellFactory(fromDateCellFactory);
fromDate.valueProperty().addListener(fromDateListener);
fromDate.setConverter(converter);
toDate.valueProperty().addListener(toDateListener);
toDate.setConverter(converter);
}
public void makeLayout() {
setHgap(6);
add(fromLabel, 0, 0);
add(fromDate, 1, 0);
add(toLabel, 2, 0);
add(toDate, 3, 0);
fromDate.setPrefWidth(120);
toDate.setPrefWidth(120);
fromLabel.setId("calendar-label");
toLabel.setId("calendar-label");
}
public void i18n() {
// i18n code replaced with
fromDate.setPromptText("dd.mm.yyyy");
toDate.setPromptText("dd.mm.yyyy");
fromLabel.setText("From");
toLabel.setText("To");
todayTooltip.setText("Today");
}
public void initComponent() {
fromDate.setTooltip(null); // Ønsker ikke tooltip
setDateWithoutFiringEvent(controller.currentDate(), fromDate);
fromDate.setShowWeekNumbers(DISPLAY_WEEK_NUMBER);
toDate.setTooltip(null); // Ønsker ikke tooltip
setDateWithoutFiringEvent(controller.currentDate(), toDate);
toDate.setShowWeekNumbers(DISPLAY_WEEK_NUMBER);
}
}
/** period-picker.css goes udner resources (using maven) **/
.date-picker {
/* -fx-font-size: 11pt;*/
}
.calendar-before {
}
.calendar-between {
-fx-background-color: #bce9ff;
}
.calendar-between:hover {
-fx-background-color: rgb(0, 150, 201);
}
.calendar-between:focused {
-fx-background-color: rgb(0, 150, 201);
}
.calendar-today {
-fx-background-color: rgb(255, 218, 111);
}
.calendar-today:hover {
-fx-background-color: rgb(0, 150, 201);
}
.calendar-today:focused {
-fx-background-color: rgb(0, 150, 201);
}
#calendar-label {
-fx-font-style: italic;
-fx-fill: rgb(75, 75, 75);
-fx-font-size: 11;
}
I think you are already in the right track... DateCell and drag could work, since the popup is not closed if a dragging event is detected or when it ends. That gives you the opportunity to track the cells selected by the user.
This is a quick hack, but it may help you with the range selection.
First it will get the content and a list of all the cells within the displayed month, adding a listener to drag events, marking as the first cell that where the drag starts, and selecting all the cells within this first cell and the cell under the actual mouse position, deselecting the rest.
After the drag event finished, the selected range is shown on the console. And you can start all over again, until the popup is closed.
private DateCell iniCell=null;
private DateCell endCell=null;
#Override
public void start(Stage primaryStage) {
DatePicker datePicker=new DatePicker();
datePicker.setValue(LocalDate.now());
Scene scene = new Scene(new AnchorPane(datePicker), 300, 250);
primaryStage.setScene(scene);
primaryStage.show();
datePicker.showingProperty().addListener((obs,b,b1)->{
if(b1){
DatePickerContent content = (DatePickerContent)((DatePickerSkin)datePicker.getSkin()).getPopupContent();
List<DateCell> cells = content.lookupAll(".day-cell").stream()
.filter(ce->!ce.getStyleClass().contains("next-month"))
.map(n->(DateCell)n)
.collect(Collectors.toList());
content.setOnMouseDragged(e->{
Node n=e.getPickResult().getIntersectedNode();
DateCell c=null;
if(n instanceof DateCell){
c=(DateCell)n;
} else if(n instanceof Text){
c=(DateCell)(n.getParent());
}
if(c!=null && c.getStyleClass().contains("day-cell") &&
!c.getStyleClass().contains("next-month")){
if(iniCell==null){
iniCell=c;
}
endCell=c;
}
if(iniCell!=null && endCell!=null){
int ini=(int)Math.min(Integer.parseInt(iniCell.getText()),
Integer.parseInt(endCell.getText()));
int end=(int)Math.max(Integer.parseInt(iniCell.getText()),
Integer.parseInt(endCell.getText()));
cells.stream()
.forEach(ce->ce.getStyleClass().remove("selected"));
cells.stream()
.filter(ce->Integer.parseInt(ce.getText())>=ini)
.filter(ce->Integer.parseInt(ce.getText())<=end)
.forEach(ce->ce.getStyleClass().add("selected"));
}
});
content.setOnMouseReleased(e->{
if(iniCell!=null && endCell!=null){
System.out.println("Selection from "+iniCell.getText()+" to "+endCell.getText());
}
endCell=null;
iniCell=null;
});
}
});
}
And this is how it looks like:
For now this doesn't update the textfield, as this involves using a custom formatter.
EDIT
I've added a custom string converter to show the range on the textfield, after a selection is done, and also to select a range if a valid one is entered.
This is not bullet proof, but it works as a proof of concept.
private DateCell iniCell=null;
private DateCell endCell=null;
private LocalDate iniDate;
private LocalDate endDate;
final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("d.MM.uuuu", Locale.ENGLISH);
#Override
public void start(Stage primaryStage) {
DatePicker datePicker=new DatePicker();
datePicker.setValue(LocalDate.now());
datePicker.setConverter(new StringConverter<LocalDate>() {
#Override
public String toString(LocalDate object) {
if(iniDate!=null && endDate!=null){
return iniDate.format(formatter)+" - "+endDate.format(formatter);
}
return object.format(formatter);
}
#Override
public LocalDate fromString(String string) {
if(string.contains("-")){
try{
iniDate=LocalDate.parse(string.split("-")[0].trim(), formatter);
endDate=LocalDate.parse(string.split("-")[1].trim(), formatter);
} catch(DateTimeParseException dte){
return LocalDate.parse(string, formatter);
}
return iniDate;
}
return LocalDate.parse(string, formatter);
}
});
Scene scene = new Scene(new AnchorPane(datePicker), 300, 250);
primaryStage.setScene(scene);
primaryStage.show();
datePicker.showingProperty().addListener((obs,b,b1)->{
if(b1){
DatePickerContent content = (DatePickerContent)((DatePickerSkin)datePicker.getSkin()).getPopupContent();
List<DateCell> cells = content.lookupAll(".day-cell").stream()
.filter(ce->!ce.getStyleClass().contains("next-month"))
.map(n->(DateCell)n)
.collect(Collectors.toList());
// select initial range
if(iniDate!=null && endDate!=null){
int ini=iniDate.getDayOfMonth();
int end=endDate.getDayOfMonth();
cells.stream()
.forEach(ce->ce.getStyleClass().remove("selected"));
cells.stream()
.filter(ce->Integer.parseInt(ce.getText())>=ini)
.filter(ce->Integer.parseInt(ce.getText())<=end)
.forEach(ce->ce.getStyleClass().add("selected"));
}
iniCell=null;
endCell=null;
content.setOnMouseDragged(e->{
Node n=e.getPickResult().getIntersectedNode();
DateCell c=null;
if(n instanceof DateCell){
c=(DateCell)n;
} else if(n instanceof Text){
c=(DateCell)(n.getParent());
}
if(c!=null && c.getStyleClass().contains("day-cell") &&
!c.getStyleClass().contains("next-month")){
if(iniCell==null){
iniCell=c;
}
endCell=c;
}
if(iniCell!=null && endCell!=null){
int ini=(int)Math.min(Integer.parseInt(iniCell.getText()),
Integer.parseInt(endCell.getText()));
int end=(int)Math.max(Integer.parseInt(iniCell.getText()),
Integer.parseInt(endCell.getText()));
cells.stream()
.forEach(ce->ce.getStyleClass().remove("selected"));
cells.stream()
.filter(ce->Integer.parseInt(ce.getText())>=ini)
.filter(ce->Integer.parseInt(ce.getText())<=end)
.forEach(ce->ce.getStyleClass().add("selected"));
}
});
content.setOnMouseReleased(e->{
if(iniCell!=null && endCell!=null){
iniDate=LocalDate.of(datePicker.getValue().getYear(),
datePicker.getValue().getMonth(),
Integer.parseInt(iniCell.getText()));
endDate=LocalDate.of(datePicker.getValue().getYear(),
datePicker.getValue().getMonth(),
Integer.parseInt(endCell.getText()));
System.out.println("Selection from "+iniDate+" to "+endDate);
datePicker.setValue(iniDate);
int ini=iniDate.getDayOfMonth();
int end=endDate.getDayOfMonth();
cells.stream()
.forEach(ce->ce.getStyleClass().remove("selected"));
cells.stream()
.filter(ce->Integer.parseInt(ce.getText())>=ini)
.filter(ce->Integer.parseInt(ce.getText())<=end)
.forEach(ce->ce.getStyleClass().add("selected"));
}
endCell=null;
iniCell=null;
});
}
});
}
By using this answer here: https://stackoverflow.com/a/60618476/9278333
I was able to create this date range selector without the use of a private api:
Usage:
MultiDatePicker multiDatePicker = new MultiDatePicker().withRangeSelectionMode();
DatePicker rangePicker = multiDatePicker.getDatePicker();
import javafx.collections.FXCollections;
import javafx.scene.control.*;
import javafx.util.StringConverter;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import static java.time.temporal.ChronoUnit.DAYS;
import java.util.LinkedHashSet;
import java.util.Objects;
import java.util.Set;
import java.util.TreeSet;
import javafx.collections.ObservableSet;
import javafx.event.EventHandler;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
public class MultiDatePicker
{
private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
private final ObservableSet<LocalDate> selectedDates;
private final DatePicker datePicker;
public MultiDatePicker()
{
this.selectedDates = FXCollections.observableSet(new TreeSet<>());
this.datePicker = new DatePicker();
setUpDatePicker();
}
public MultiDatePicker withRangeSelectionMode()
{
EventHandler<MouseEvent> mouseClickedEventHandler = (MouseEvent clickEvent) ->
{
if (clickEvent.getButton() == MouseButton.PRIMARY)
{
if (!this.selectedDates.contains(this.datePicker.getValue()))
{
this.selectedDates.add(datePicker.getValue());
this.selectedDates.addAll(getRangeGaps((LocalDate) this.selectedDates.toArray()[0], (LocalDate) this.selectedDates.toArray()[this.selectedDates.size() - 1]));
} else
{
this.selectedDates.remove(this.datePicker.getValue());
this.selectedDates.removeAll(getTailEndDatesToRemove(this.selectedDates, this.datePicker.getValue()));
this.datePicker.setValue(getClosestDateInTree(new TreeSet<>(this.selectedDates), this.datePicker.getValue()));
}
}
this.datePicker.show();
clickEvent.consume();
};
this.datePicker.setDayCellFactory((DatePicker param) -> new DateCell()
{
#Override
public void updateItem(LocalDate item, boolean empty)
{
super.updateItem(item, empty);
//...
if (item != null && !empty)
{
//...
addEventHandler(MouseEvent.MOUSE_CLICKED, mouseClickedEventHandler);
} else
{
//...
removeEventHandler(MouseEvent.MOUSE_CLICKED, mouseClickedEventHandler);
}
if (!selectedDates.isEmpty() && selectedDates.contains(item))
{
if (Objects.equals(item, selectedDates.toArray()[0]) || Objects.equals(item, selectedDates.toArray()[selectedDates.size() - 1]))
{
setStyle("-fx-background-color: rgba(3, 169, 1, 0.7);");
} else
{
setStyle("-fx-background-color: rgba(3, 169, 244, 0.7);");
}
} else
{
setStyle(null);
}
}
});
return this;
}
public ObservableSet<LocalDate> getSelectedDates()
{
return this.selectedDates;
}
public DatePicker getDatePicker()
{
return this.datePicker;
}
private void setUpDatePicker()
{
this.datePicker.setConverter(new StringConverter<LocalDate>()
{
#Override
public String toString(LocalDate date)
{
return (date == null) ? "" : DATE_FORMAT.format(date);
}
#Override
public LocalDate fromString(String string)
{
return ((string == null) || string.isEmpty()) ? null : LocalDate.parse(string, DATE_FORMAT);
}
});
EventHandler<MouseEvent> mouseClickedEventHandler = (MouseEvent clickEvent) ->
{
if (clickEvent.getButton() == MouseButton.PRIMARY)
{
if (!this.selectedDates.contains(this.datePicker.getValue()))
{
this.selectedDates.add(datePicker.getValue());
} else
{
this.selectedDates.remove(this.datePicker.getValue());
this.datePicker.setValue(getClosestDateInTree(new TreeSet<>(this.selectedDates), this.datePicker.getValue()));
}
}
this.datePicker.show();
clickEvent.consume();
};
this.datePicker.setDayCellFactory((DatePicker param) -> new DateCell()
{
#Override
public void updateItem(LocalDate item, boolean empty)
{
super.updateItem(item, empty);
//...
if (item != null && !empty)
{
//...
addEventHandler(MouseEvent.MOUSE_CLICKED, mouseClickedEventHandler);
} else
{
//...
removeEventHandler(MouseEvent.MOUSE_CLICKED, mouseClickedEventHandler);
}
if (selectedDates.contains(item))
{
setStyle("-fx-background-color: rgba(3, 169, 244, 0.7);");
} else
{
setStyle(null);
}
}
});
}
private static Set<LocalDate> getTailEndDatesToRemove(Set<LocalDate> dates, LocalDate date)
{
TreeSet<LocalDate> tempTree = new TreeSet<>(dates);
tempTree.add(date);
int higher = tempTree.tailSet(date).size();
int lower = tempTree.headSet(date).size();
if (lower <= higher)
{
return tempTree.headSet(date);
} else if (lower > higher)
{
return tempTree.tailSet(date);
} else
{
return new TreeSet<>();
}
}
private static LocalDate getClosestDateInTree(TreeSet<LocalDate> dates, LocalDate date)
{
Long lower = null;
Long higher = null;
if (dates.isEmpty())
{
return null;
}
if (dates.size() == 1)
{
return dates.first();
}
if (dates.lower(date) != null)
{
lower = Math.abs(DAYS.between(date, dates.lower(date)));
}
if (dates.higher(date) != null)
{
higher = Math.abs(DAYS.between(date, dates.higher(date)));
}
if (lower == null)
{
return dates.higher(date);
} else if (higher == null)
{
return dates.lower(date);
} else if (lower <= higher)
{
return dates.lower(date);
} else if (lower > higher)
{
return dates.higher(date);
} else
{
return null;
}
}
private static Set<LocalDate> getRangeGaps(LocalDate min, LocalDate max)
{
Set<LocalDate> rangeGaps = new LinkedHashSet<>();
if (min == null || max == null)
{
return rangeGaps;
}
LocalDate lastDate = min.plusDays(1);
while (lastDate.isAfter(min) && lastDate.isBefore(max))
{
rangeGaps.add(lastDate);
lastDate = lastDate.plusDays(1);
}
return rangeGaps;
}
}

Google Maps API v3 - Buttons and TextBoxes inside InfoWindow?

I'm using the new maps v3 API from gwt-google-apis.
Is it possible to capture events from GWT widgets that are inside InfoWindow? Am I missing something?
Tried code above (button.addClickHandler) and it doesn't show the alert:
Marker m = Marker.create();
m.setIcon(MarkerImage.create(icone));
m.setPosition(LatLng.create(posicao.lat(), posicao.lng()));
m.setMap(map);
m.addClickHandler(new ClickHandler() {
#Override
public void handle(MouseEvent event) {
InfoWindow info = InfoWindow.create();
Button button = new Button("Desativar");
button.addClickHandler(new com.google.gwt.event.dom.client.ClickHandler() {
#Override
public void onClick(ClickEvent event) {
Window.alert("click");
}
});
final HTMLPanel html = new HTMLPanel(("<div id='button'></div>"));
html.add(button, "button");
info.setContent(html.getElement());
info.setPosition(posicao);
info.open(map);
}
});
Thanks.
The problem is result of a broken hierarchy between the widgets, the normal way to do it is by attach / detach widget. You do it by setting of the widget's element. This is also matter of Google Maps API.
This can be resolved by using fake panel which will be part of the InfoWindow, so when you make setContent(Widget widget) the fake panel will be updated and the element of the widget will be set to the content (as previous).
Please take a look at this class:
public class MyInfoWindow extends InfoWindow {
static class FakePanel extends ComplexPanel {
public FakePanel(Widget w) {
w.removeFromParent();
getChildren().add(w);
adopt(w);
}
#Override
public boolean isAttached() {
return true;
}
public void detachWidget() {
this.remove(0);
}
}
private IsWidget widgetContent = null;
FakePanel widgetAttacher;
public MyInfoWindow() {
super(InfoWindowImpl.impl.construct());
}
private void detachWidget() {
if (this.widgetAttacher != null) {
this.widgetAttacher.detachWidget();
this.widgetAttacher = null;
}
}
public void close() {
super.close();
detachWidget();
}
public void setContent(String content) {
this.widgetContent = null;
this.detachWidget();
super.setContent(content);
}
/** */
public void setContent(Widget value) {
this.widgetContent = value;
setContent(value.getElement());
if (this.widgetAttacher == null) {
addListener(getJso(), "closeclick", new Runnable() {
#Override
public void run() {
detachWidget();
}
});
this.widgetAttacher = new FakePanel(value);
} else if (this.widgetAttacher.getWidget(0) != value) {
this.widgetAttacher.detachWidget();
this.widgetAttacher = new FakePanel(value);
}
}
private void setContent(Element element) {
InfoWindowImpl.impl.setContent(getJso(), element);
}
public IsWidget getContentWidget() {
return widgetContent;
}
public final native void addListener(JavaScriptObject jso, String whichEvent, Runnable handler)
/*-{
var that = jso;
$wnd.google.maps.event.addListener(jso, whichEvent, function() {
handler.#java.lang.Runnable::run()();
});
}-*/;
}
I had to build a wrapper over InfoWindow to make it work.
public class NXInfoWindow {
static class FakePanel extends ComplexPanel {
public FakePanel(Widget w) {
w.removeFromParent();
getChildren().add(w);
adopt(w);
}
#Override
public boolean isAttached() {
return true;
}
public void detachWidget() {
this.remove(0);
}
}
private InfoWindow info;
private IsWidget widgetContent = null;
private Long id;
FakePanel widgetAttacher;
public static NXInfoWindow create(Long id){
NXInfoWindow myInfo = new NXInfoWindow();
myInfo.info = InfoWindow.create();
myInfo.id = id;
return myInfo;
};
private void detachWidget() {
if (this.widgetAttacher != null) {
this.widgetAttacher.detachWidget();
this.widgetAttacher = null;
}
}
public void close() {
info.close();
detachWidget();
}
public void setPosition(LatLng posicao) {
info.setPosition(posicao);
}
public void open(GoogleMap map) {
info.open(map);
}
public void setContent(Widget value) {
this.widgetContent = value;
info.setContent(value.getElement());
if (this.widgetAttacher == null) {
addListener(info, "closeclick", new Runnable() {
#Override
public void run() {
detachWidget();
}
});
this.widgetAttacher = new FakePanel(value);
} else if (this.widgetAttacher.getWidget(0) != value) {
this.widgetAttacher.detachWidget();
this.widgetAttacher = new FakePanel(value);
}
}
private void setContent(Element element) {
this.setContent(element);
}
public IsWidget getContentWidget() {
return widgetContent;
}
public final native void addListener(JavaScriptObject jso, String whichEvent, Runnable handler)
/*-{
var that = jso;
$wnd.google.maps.event.addListener(jso, whichEvent, function() {
handler.#java.lang.Runnable::run()();
});
}-*/;
}

GWT - Add and remove nodes in celltree

Here I have an complete and very easy example to dynamically add/remove
nodes to an celltree. My example is not working very well. It seems there
is an refresh problem. Only closing/expanding the nodes will show the correct
result. I also did not found any answer in this forum which fits to this problem.
Maybe somebody can try my example and tell me where the problem is.
Any other hint is also very appreciated.
Greetings, Marco
import java.util.ArrayList;
import com.google.gwt.cell.client.AbstractCell;
import com.google.gwt.core.client.EntryPoint;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.safehtml.shared.SafeHtmlBuilder;
import com.google.gwt.user.cellview.client.CellTree;
import com.google.gwt.user.client.ui.AbsolutePanel;
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.RootPanel;
import com.google.gwt.view.client.ListDataProvider;
import com.google.gwt.view.client.SingleSelectionModel;
import com.google.gwt.view.client.TreeViewModel;
public class MyCelltreeTest implements EntryPoint {
private AbsolutePanel absolutePanel;
private CellTree cellTree;
private Button btnAdd;
private Button btnRemove;
private MyTreeModel myTreeModel;
private SingleSelectionModel<MyNode> selectionModelCellTree = null;
#Override
public void onModuleLoad() {
RootPanel rootPanel = RootPanel.get();
rootPanel.add(getAbsolutePanel(), 0, 0);
}
private AbsolutePanel getAbsolutePanel() {
if (absolutePanel == null) {
absolutePanel = new AbsolutePanel();
absolutePanel.setSize("612px", "482px");
absolutePanel.add(getCellTree(), 0, 0);
absolutePanel.add(getBtnAdd(), 265, 428);
absolutePanel.add(getBtnRemove(), 336, 428);
}
return absolutePanel;
}
private CellTree getCellTree() {
if (cellTree == null) {
myTreeModel = new MyTreeModel();
cellTree = new CellTree(myTreeModel, null);
cellTree.setSize("285px", "401px");
}
return cellTree;
}
private Button getBtnAdd() {
if (btnAdd == null) {
btnAdd = new Button("Add");
btnAdd.addClickHandler(new ClickHandler() {
#Override
public void onClick(ClickEvent event) {
MyNode node = selectionModelCellTree.getSelectedObject();
if(node != null)
myTreeModel.addNew(node, "Bla");
}
});
}
return btnAdd;
}
private Button getBtnRemove() {
if (btnRemove == null) {
btnRemove = new Button("Remove");
btnRemove.addClickHandler(new ClickHandler() {
#Override
public void onClick(ClickEvent event) {
MyNode node = selectionModelCellTree.getSelectedObject();
if(node != null)
myTreeModel.remove(node);
}
});
}
return btnRemove;
}
public class MyNode {
private String name;
private ArrayList<MyNode> childs; //nodes childrens
private MyNode parent; //track internal parent
private MyCell cell; //for refresh - reference to visual component
public MyNode(String name) {
super();
parent = null;
this.name = name;
childs = new ArrayList<MyNode>();
}
public void addSubMenu(MyNode m) {
m.parent = this;
childs.add(m);
}
public void removeMenu(MyNode m) {
m.getParent().childs.remove(m);
}
public boolean hasChildrens() {
return childs.size()>0;
}
public ArrayList<MyNode> getList() {
return childs;
}
public MyNode getParent() {
return parent;
}
public void setCell(MyCell cell) {
this.cell = cell;
}
public void refresh() {
if(parent!=null) {
parent.refresh();
}
if (cell!=null) {
cell.refresh(); //refresh tree
}
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
public class MyTreeModel implements TreeViewModel {
private MyNode officialRoot; //default not dynamic
private MyNode studentRoot; //default not dynamic
private MyNode testRoot; //default not dynamic
private MyNode root;
public MyNode getRoot() { // to set CellTree root
return root;
}
public MyTreeModel() {
selectionModelCellTree = new SingleSelectionModel<MyNode>();
root = new MyNode("root");
// Default items
officialRoot = new MyNode("Cat"); //some basic static data
studentRoot = new MyNode("Dog");
testRoot = new MyNode("Fish");
root.addSubMenu(officialRoot);
root.addSubMenu(studentRoot);
root.addSubMenu(testRoot);
}
//example of add add logic
public void addNew(MyNode myparent, String name) {
myparent.addSubMenu(new MyNode(name));
myparent.refresh(); //HERE refresh tree
}
public void remove(MyNode objToRemove) {
objToRemove.removeMenu(objToRemove);
objToRemove.refresh();
}
#Override
public <T> NodeInfo<?> getNodeInfo(T value) {
ListDataProvider<MyNode> dataProvider;
MyNode myValue = null;
if (value == null) { // root is not set
dataProvider = new ListDataProvider<MyNode>(root.getList());
} else {
myValue = (MyNode) value;
dataProvider = new ListDataProvider<MyNode>(myValue.getList());
}
MyCell cell = new MyCell(dataProvider); //HERE Add reference
if (myValue != null)
myValue.setCell(cell);
return new DefaultNodeInfo<MyNode>(dataProvider, cell, selectionModelCellTree, null);
}
#Override
public boolean isLeaf(Object value) {
if (value instanceof MyNode) {
MyNode t = (MyNode) value;
if (!t.hasChildrens())
return true;
return false;
}
return false;
}
}
public class MyCell extends AbstractCell<MyNode> {
ListDataProvider<MyNode> dataProvider; //for refresh
public MyCell(ListDataProvider<MyNode> dataProvider) {
super();
this.dataProvider = dataProvider;
}
public void refresh() {
dataProvider.refresh();
}
#Override
public void render(Context context, MyNode value, SafeHtmlBuilder sb) {
if (value == null) {
return;
}
sb.appendEscaped(value.getName());
}
}
}
Thanks Ümit for your explanation.
I tried the close-reopen version.
I have replaced my refresh method with the methods below.
Adding is working but removing not.
Very strange the whole topic. I'm very suprised that these basic
functions are not really supported by GWT.
Can somebody give me more help to run a real working example.
public void refresh() {
closeReopenTreeNodes(cellTree.getRootTreeNode());
}
private void closeReopenTreeNodes(TreeNode node) {
if(node == null) {
return;
}
for(int i = 0; i < node.getChildCount(); i++) {
if(node.getChildValue(i).equals(this)){
if(node.getParent() != null){
node.getParent().setChildOpen(i, false);
//node.getParent().setChildOpen(i, true);
}
node.setChildOpen(i, false);
node.setChildOpen(i, true);
}
TreeNode child = node.setChildOpen(i, node.isChildOpen(i));
closeReopenTreeNodes(child);
}
}
Here my third try:
This way is recommended by gwt-commiter.
Please see following issue:
http://code.google.com/p/google-web-toolkit/issues/detail?id=7160
Current status:
* Adding is possible
* Removing is possible if not last child!
So, last open point, refresh the tree if last open child!
package com.test;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import com.google.gwt.cell.client.AbstractCell;
import com.google.gwt.core.client.EntryPoint;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.safehtml.shared.SafeHtmlBuilder;
import com.google.gwt.user.cellview.client.CellTree;
import com.google.gwt.user.client.ui.AbsolutePanel;
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.RootPanel;
import com.google.gwt.view.client.ListDataProvider;
import com.google.gwt.view.client.SingleSelectionModel;
import com.google.gwt.view.client.TreeViewModel;
public class MyCelltreeTest2 implements EntryPoint {
private AbsolutePanel absolutePanel;
private CellTree cellTree;
private Button btnAdd;
private Button btnRemove;
private MyTreeModel myTreeModel;
private SingleSelectionModel<MyNode> selectionModelCellTree = null;
private Map<MyNode, ListDataProvider<MyNode>> mapDataProviders = null;
private ListDataProvider<MyNode> rootDataProvider = null;
public void onModuleLoad() {
RootPanel rootPanel = RootPanel.get();
rootPanel.add(getAbsolutePanel(), 0, 0);
}
private AbsolutePanel getAbsolutePanel() {
if (absolutePanel == null) {
absolutePanel = new AbsolutePanel();
absolutePanel.setSize("612px", "482px");
absolutePanel.add(getCellTree(), 0, 0);
absolutePanel.add(getBtnAdd(), 265, 428);
absolutePanel.add(getBtnRemove(), 336, 428);
}
return absolutePanel;
}
private CellTree getCellTree() {
if (cellTree == null) {
myTreeModel = new MyTreeModel();
cellTree = new CellTree(myTreeModel, null);
cellTree.setSize("285px", "401px");
}
return cellTree;
}
private Button getBtnAdd() {
if (btnAdd == null) {
btnAdd = new Button("Add");
btnAdd.addClickHandler(new ClickHandler() {
public void onClick(ClickEvent event) {
MyNode node = selectionModelCellTree.getSelectedObject();
myTreeModel.add(node, "Bla");
}
});
}
return btnAdd;
}
private Button getBtnRemove() {
if (btnRemove == null) {
btnRemove = new Button("Remove");
btnRemove.addClickHandler(new ClickHandler() {
public void onClick(ClickEvent event) {
MyNode node = selectionModelCellTree.getSelectedObject();
myTreeModel.remove(node);
}
});
}
return btnRemove;
}
public class MyNode {
private String name;
private ArrayList<MyNode> childs; //nodes childrens
private MyNode parent; //track internal parent
public MyNode(String name) {
super();
parent = null;
this.name = name;
childs = new ArrayList<MyNode>();
}
public boolean hasChildrens() {
return childs.size()>0;
}
public ArrayList<MyNode> getList() {
return childs;
}
public MyNode getParent() {
return parent;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
public class MyTreeModel implements TreeViewModel {
public MyTreeModel() {
selectionModelCellTree = new SingleSelectionModel<MyNode>();
mapDataProviders = new HashMap<MyCelltreeTest2.MyNode, ListDataProvider<MyNode>>();
}
public void add(MyNode myparent, String name) {
MyNode child = new MyNode(name);
//root-node
if(myparent == null){
rootDataProvider.getList().add(child);
mapDataProviders.put(child, rootDataProvider);
}
else{
ListDataProvider<MyNode> dataprovider = mapDataProviders.get(myparent);
myparent.childs.add(child);
child.parent = myparent;
dataprovider.refresh();
}
}
public void remove(MyNode objToRemove) {
ListDataProvider<MyNode> dataprovider = mapDataProviders.get(objToRemove);
dataprovider.getList().remove(objToRemove);
// mapDataProviders.remove(objToRemove);
dataprovider.refresh();
dataprovider.flush();
if(objToRemove.parent != null){
ListDataProvider<MyNode> dataproviderParent = mapDataProviders.get(objToRemove.parent);
objToRemove.parent.childs.remove(objToRemove);
dataproviderParent.refresh();
dataproviderParent.flush();
}
else{
rootDataProvider.refresh();
rootDataProvider.flush();
}
}
#Override
public <T> NodeInfo<?> getNodeInfo(T value) {
if (value == null) { // root is not set
rootDataProvider = new ListDataProvider<MyNode>(new ArrayList<MyNode>());
MyCell cell = new MyCell();
return new DefaultNodeInfo<MyNode>(rootDataProvider, cell,
selectionModelCellTree, null);
} else {
MyNode myValue = (MyNode) value;
ListDataProvider<MyNode> dataProvider =
new ListDataProvider<MyNode>(myValue.childs);
MyCell cell = new MyCell();
for(MyNode currentNode : myValue.childs){
mapDataProviders.put(currentNode, dataProvider);
}
return new DefaultNodeInfo<MyNode>(dataProvider, cell,
selectionModelCellTree, null);
}
}
#Override
public boolean isLeaf(Object value) {
if (value instanceof MyNode) {
MyNode t = (MyNode) value;
if (!t.hasChildrens())
return true;
return false;
}
return false;
}
}
public class MyCell extends AbstractCell<MyNode> {
public MyCell() {
super();
}
#Override
public void render(Context context, MyNode value, SafeHtmlBuilder sb) {
if (value == null) {
return;
}
sb.appendEscaped(value.getName());
}
}
}
This is somehow a known problem with CellTree.
The reason for the refresh problem is that in the getNodeInfo() function you create a new ListDataProvider instance for each CellTree level.
The CellTree only updates/refreshes itself if you update the items in that ListDataProvider.
I believe that your removeMenu() and addSubMenu() functions add and remove items from the original list stored in your MyNode class but won't update the list in the corresponding ListDataProviders (you can try to check that in debug mode).
The reason why the CellTree is refreshed when you close and re
-open the nodes is because when you re-open the nodes the getNodeInfo() is called again and the whole CellTree structure will be constructed again (including the new menu or without the removed one respectively).
There are two possible solutions:
Keep a reference for each of the ListDataProviders somewhere and call remove/add on that list (although you do that I assume that the items are not really added/removed there).
Programatically close all nodes and re-open it.
Both are somehow a PITA to implement. Unfortunately there is no easy way around it.
I just clear the array of objects maintained in my data provider. I do this in onRangeChanged(final HasData<?> display). I guess I don't use a ListDataProvider here, I use something extending AbstractDataProvider<T> instead.
To add the node add it to your list within the onRangeChanged() method and then call updateRowData(). You can do this for a delete too.
I think I may have licked the problem...
Essentially I've extended and subclasses many parts of CellTree and have obtained an almost perfect working example. Too complex to document here, but suffice to say the solution involved using a node class where I stored the data provider within each node.
https://code.google.com/p/updatable-cell-tree/