My goal is to display every field of an instance of a Class in a tableView. The Class has a field of type enum which has a field of type String.
The enum shall be displayed in a ComboBox as it's String field name.
Of course it also has to be editable.
Now what isn't working:
The String field of enum class is only displayed if the ComboBox is clicked on, otherwise it is the name of the enum constant. Also, if another enum in the combobox is selected, it can't be commited for edit. Clicking return doesn't deselect the Combobox neither is the method commitEdit invoked. If an other column is selected for edit, the attempted edit is cancelled.
I put some effort into trying to figure this out, so I thought maybe one could help me here.
As the original task is about much bigger classes in enterprise software, I abstracted it to ask this question.
I know I could make the column holding the enum of type String and make it work with MyEnum.values() and MyEnum.valueOf() but that could not go into production due to bad performance as the original classes are too big.
Here is my code as an example, if you don't understand the problems just try to use the combobox once and you'll see.
Class which is type of TableView:
public class MyClass {
private MyEnum myEnum;
private String string;
public MyClass(MyEnum myEnum, String string) {
this.myEnum = myEnum;
this.string = string;
}
public MyEnum getMyEnum() {
return myEnum;
}
public void setMyEnum(MyEnum myEnum) {
this.myEnum = myEnum;
}
public String getString() {
return string;
}
}
It's enum field:
public enum MyEnum {
EnumOne("First Enum"),
EnumTwo("Second Enum");
private String name;
public String getName() {
return name;
}
private MyEnum(String name) {
this.name = name;
}
}
FX App:
public class NewFXMain extends Application {
#Override
public void start(Stage primaryStage) {
ObservableList<MyClass> items = FXCollections.observableArrayList();
items.add(new MyClass(MyEnum.EnumOne, "String"));
TableView<MyClass> table = new TableView(items);
table.setEditable(true);
TableColumn<MyClass, MyEnum> myEnumColumn = new TableColumn();
TableColumn<MyClass, String> stringColumn = new TableColumn();
stringColumn.setCellFactory(TextFieldTableCell.forTableColumn());
stringColumn.setCellValueFactory(data -> new ReadOnlyStringWrapper(data.getValue().getString()));
myEnumColumn.setCellFactory((param) -> new MyEnumComboBoxCell());
myEnumColumn.setCellValueFactory(data -> new ReadOnlyObjectWrapper(data.getValue().getMyEnum()));
myEnumColumn.setOnEditCommit(
event -> {
event.getRowValue().setMyEnum(event.getNewValue());
System.out.println(event.getRowValue().getMyEnum());
});
table.getColumns().addAll(myEnumColumn, stringColumn);
StackPane root = new StackPane();
root.getChildren().add(table);
Scene scene = new Scene(root, 300, 250);
primaryStage.setScene(scene);
primaryStage.show();
}
/**
* #param args the command line arguments
*/
public static void main(String[] args) {
launch(args);
}
}
class MyEnumComboBoxCell extends ComboBoxTableCell<MyClass, MyEnum> {
private ComboBox<MyEnum> box;
public MyEnumComboBoxCell() {
box = new ComboBox<>(FXCollections.observableArrayList(MyEnum.values()));
box.setCellFactory(new Callback<ListView<MyEnum>, ListCell<MyEnum>>() {
#Override
public ListCell<MyEnum> call(ListView<MyEnum> param) {
return new ListCell<MyEnum>() {
#Override
protected void updateItem(MyEnum item, boolean empty) {
super.updateItem(item, empty);
if ( item != null ) setText(item.getName());
}
};
}
});
}
#Override
public void startEdit() {
super.startEdit();
setGraphic(box);
}
#Override
public void commitEdit(MyEnum newValue) {
super.commitEdit(newValue);
if ( newValue != null ) {
setText(newValue.getName());
getTableView().getSelectionModel().getSelectedItem().setMyEnum(newValue);
box.setValue(newValue);
}
}
#Override
public void updateItem(MyEnum item, boolean empty) {
super.updateItem(item, empty);
if ( empty ) {
setGraphic(null);
} else {
setGraphic(null);
setText(item.getName());
}
}
}
Instead of setting the name in updateItem use a StringConverter like:
public class MyEnumConverter extends StringConverter<MyEnum>{
#Override public String toString(MyEnum enumConstant) {
return enumConstant.getName();
}
#Override public MyEnum fromString(String string) {
return MyEnum.valueOf(string);
}
}
Then in the cell's constructor:
this.setConverter(new MyEnumConverter());
Edit: You may not #Override all of the ComboBoxTableCell's methods, since all of them are working like you want. On the other hand you should not specify an own ComboBox for the table cell, since it has one. You just have to add a StringConverter and set the items.
You may use like this:
myEnumColumn.setCellFactory((param) -> new ComboBoxTableCell<>(new StringConverter<MyEnum>() {
#Override public String toString(MyEnum object) {
return object.getName();
}
#Override public MyEnum fromString(String string) {
return MyEnum.valueOf(string);
}
}, MyEnum.values()));
If you prefer you can create a separate class for the StringConverter like I mentioned earlier, then just simply:
myEnumColumn.setCellFactory(factory -> new ComboBoxTableCell<>(new MyEnumConverter(), MyEnum.values()));
You can also get rid of the myEnumColumn.setOnEditCommit.
Thanks a lot! Actually I've spent a day on this, partially with another person, so this is really appreciated! :)
But:
I have to implement setOnEditCommit or otherwise the myEnum field backing the tableColumn does not change! With this everything works. Without, only what is displayed is changed.
myEnumColumn.setOnEditCommit(
event ->
{
event.getRowValue().setMyEnum(event.getNewValue());
});
Related
My use-case:
a custom property on a control that should be configurable via css
the property must be changeable at runtime
for a given instance of the control, the programmatic change must not be reverted on re-applying the css
A custom StyleableProperty looks like a perfect match to implement the requirement. Below is an example that implements (taken without change from the class javadoc of StyleablePropertyFactory).
All is fine except for the last requirement: on applyCss, the default value from the stylesheet is reapplied. To reproduce:
run the example, note that the initial "selected" state (the checkbox' selected is bound it) of the MyButton is true
click the custom button, note that the "selected" doesn't change to false (though the actionHandler changes it)
click on the second ("toggle") button, note that the selected state of the custom button changes to false
hover the mouse over the custom button, note that the selected state falls back to true
The reason for falling back to true (the value set via style), can be traced to applyCss which happens on state changes ... which is understandable and might be the correct thingy-to-do most of the times, but not in my context.
So the questions:
am I on the right track with using StyleableProperty?
if so, how to tweak such that it's not re-apply after a manual change has happened?
if not, what else to do?
or maybe asking the wrong questions altogether: maybe properties which are settable via css are not meant to be (permanently) changed by code?
The example:
public class StyleableButtonDriver extends Application {
/**
* example code from class doc of StyleablePropertyFactory.
*/
private static class MyButton extends Button {
private static final StyleablePropertyFactory<MyButton> FACTORY
= new StyleablePropertyFactory<>(Button.getClassCssMetaData());
MyButton(String labelText) {
super(labelText);
getStyleClass().add("my-button");
setStyle("-my-selected: true");
}
// Typical JavaFX property implementation
public ObservableValue<Boolean> selectedProperty() { return (ObservableValue<Boolean>)selected; }
public final boolean isSelected() { return selected.getValue(); }
public final void setSelected(boolean isSelected) { selected.setValue(isSelected); }
// StyleableProperty implementation reduced to one line
private final StyleableProperty<Boolean> selected =
FACTORY.createStyleableBooleanProperty(
this, "selected", "-my-selected", s -> s.selected);
#Override
public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
return FACTORY.getCssMetaData();
}
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
return FACTORY.getCssMetaData();
}
}
private Parent createContent() {
MyButton button = new MyButton("styleable button");
button.setOnAction(e -> {
// does not work: reset on applyCss
boolean isSelected = button.isSelected();
button.setSelected(!isSelected);
});
CheckBox box = new CheckBox("button selected");
box.selectedProperty().bind(button.selectedProperty());
Button toggle = new Button("toggle button");
toggle.setOnAction(e -> {
boolean isSelected = button.isSelected();
button.setSelected(!isSelected);
});
BorderPane content = new BorderPane(button);
content.setBottom(new HBox(10, box, toggle));
return content;
}
#Override
public void start(Stage stage) throws Exception {
stage.setScene(new Scene(createContent(), 300, 200));
//same behavior as setting the style directly
// URL uri = getClass().getResource("xstyleable.css");
// stage.getScene().getStylesheets().add(uri.toExternalForm());
// not useful: would have to override all
// Application.setUserAgentStylesheet(uri.toExternalForm());
stage.setTitle(FXUtils.version());
stage.show();
}
public static void main(String[] args) {
launch(args);
}
#SuppressWarnings("unused")
private static final Logger LOG = Logger
.getLogger(StyleableButtonDriver.class.getName());
}
You are on the right track, but since you need to override the default priority of the style origins (user agent stylesheet < programmatically assigned < css stylesheet < Node.style property), you cannot use SyleablePropertyFactory for creating this property. You need to create a CssMetaData object that indicates a property as non-setable, if the property was programatically assigned.
private static class MyButton extends Button {
private static final List<CssMetaData<? extends Styleable, ?>> CLASS_CSS_METADATA;
private static final CssMetaData<MyButton, Boolean> SELECTED;
static {
SELECTED = new CssMetaData<MyButton, Boolean>("-my-selected", StyleConverter.getBooleanConverter()) {
#Override
public boolean isSettable(MyButton styleable) {
// not setable, if bound or set by user
return styleable.selected.getStyleOrigin() != StyleOrigin.USER && !styleable.selected.isBound();
}
#Override
public StyleableProperty<Boolean> getStyleableProperty(MyButton styleable) {
return styleable.selected;
}
};
// copy list of button css metadata to list and add new metadata object
List<CssMetaData<? extends Styleable, ?>> buttonData = Button.getClassCssMetaData();
List<CssMetaData<? extends Styleable, ?>> mybuttonData = new ArrayList<>(buttonData.size()+1);
mybuttonData.addAll(buttonData);
mybuttonData.add(SELECTED);
CLASS_CSS_METADATA = Collections.unmodifiableList(mybuttonData);
}
MyButton(String labelText) {
super(labelText);
getStyleClass().add("my-button");
setStyle("-my-selected: true");
}
// Typical JavaFX property implementation
public ObservableValue<Boolean> selectedProperty() { return selected; }
public final boolean isSelected() { return selected.get(); }
public final void setSelected(boolean isSelected) { selected.set(isSelected); }
// StyleableProperty implementation reduced to one line
private final SimpleStyleableBooleanProperty selected = new SimpleStyleableBooleanProperty(SELECTED, this, "selected");
#Override
public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
return CLASS_CSS_METADATA;
}
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
return CLASS_CSS_METADATA;
}
}
Is there any simple approach to let the TreeTableView (or TableView) try to commit values on focus lost?
Unfortunatly I didn't succed with any default-implementations of javafx TableCellFactories, which is why I tried my own TreeTableCell implementations and also some different tableCell implementations like the one from Graham Smith, which seemed the most straight forward, since it already implemented a hook for focus lost, but nevertheless the value is never committed and the userchanges are resetted to the original value.
My guess is, whenever focus is lost, the editingProperty of the affected Cell is always already false which causes the Cell never to commit a value on focusLost. Here the relevant part from the original (oracle-)TreeTableCell Implementation (8u20ea), which causes my approaches to fail:
#Override public void commitEdit(T newValue) {
if (! isEditing()) return; // <-- here my approaches are blocked, because on focus lost its not editing anymore.
final TreeTableView<S> table = getTreeTableView();
if (table != null) {
#SuppressWarnings("unchecked")
TreeTablePosition<S,T> editingCell = (TreeTablePosition<S,T>) table.getEditingCell();
// Inform the TableView of the edit being ready to be committed.
CellEditEvent<S,T> editEvent = new CellEditEvent<S,T>(
table,
editingCell,
TreeTableColumn.<S,T>editCommitEvent(),
newValue
);
Event.fireEvent(getTableColumn(), editEvent);
}
// inform parent classes of the commit, so that they can switch us
// out of the editing state.
// This MUST come before the updateItem call below, otherwise it will
// call cancelEdit(), resulting in both commit and cancel events being
// fired (as identified in RT-29650)
super.commitEdit(newValue);
// update the item within this cell, so that it represents the new value
updateItem(newValue, false);
if (table != null) {
// reset the editing cell on the TableView
table.edit(-1, null);
// request focus back onto the table, only if the current focus
// owner has the table as a parent (otherwise the user might have
// clicked out of the table entirely and given focus to something else.
// It would be rude of us to request it back again.
ControlUtils.requestFocusOnControlOnlyIfCurrentFocusOwnerIsChild(table);
}
}
I succeded with overriding this method and commiting the value "by hand" before the original commitEdit() method is called, but this causes the commit on keys like enter to commit the value twice (on key + on focus lost). Moreover I dont really like my approach at all, so I wonder, if anyone else has solved this in a "nicer" way?
After some digging, turned out that the culprit (aka: the collaborator that cancels the edit before the textField looses focus) is the TableCellBehaviour/Base in its processing of a mousePressed:
mousePressed calls simpleSelect(..)
on detecting a single click it calls edit(-1, null)
which calls the same method on TableView
which sets its editingCell property to null
a tableCell listens to that property and reacts by canceling its own edit
Unfortunately, a hackaround requires 3 collaborators
a TableView with additional api to terminate an edit
a TableCellBehaviour with overridden simpleSelect(...) that calls the additional api (instead of edit(-1..)) before calling super
a TableCell that is configured with the extended behaviour and is aware of table's extended properties
Some code snippets (full code) :
// on XTableView:
public void terminateEdit() {
if (!isEditing()) return;
// terminatingCell is a property that supporting TableCells can listen to
setTerminatingCell(getEditingCell());
if (isEditing()) throw new IllegalStateException(
"expected editing to be terminated but was " + getEditingCell());
setTerminatingCell(null);
}
// on XTableCellBehaviour: override simpleSelect
#Override
protected void simpleSelect(MouseEvent e) {
TableCell<S, T> cell = getControl();
TableView<S> table = cell.getTableColumn().getTableView();
if (table instanceof XTableView) {
((XTableView<S>) table).terminateEdit();
}
super.simpleSelect(e);
}
// on XTextFieldTableCell - this method is called from listener
// to table's terminatingCell property
protected void terminateEdit(TablePosition<S, ?> newPosition) {
if (!isEditing() || !match(newPosition)) return;
commitEdit();
}
protected void commitEdit() {
T edited = getConverter().fromString(myTextField.getText());
commitEdit(edited);
}
/**
* Implemented to create XTableCellSkin which supports terminating edits.
*/
#Override
protected Skin<?> createDefaultSkin() {
return new XTableCellSkin<S, T>(this);
}
Note: the implementation of TableCellBehaviour changed massively between jdk8u5 and jdk8u20 (joys of hacking - not fit for production use ;-) - the method to override in the latter is handleClicks(..)
BTW: massive votingfor JDK-8089514 (was RT-18492 in old jira) might speed up a core fix. Unfortunately, at least the author role is needed to vote/comment bugs in the new tracker.
I also needed this functionality and did some study. I faced some stability issues with XTableView hacking mentioned above.
As problem seems to be commitEdit() won't take effect when focus is lost, why you don't just call your own commit callback from TableCell as follows:
public class SimpleEditingTextTableCell extends TableCell {
private TextArea textArea;
Callback commitChange;
public SimpleEditingTextTableCell(Callback commitChange) {
this.commitChange = commitChange;
}
#Override
public void startEdit() {
...
getTextArea().focusedProperty().addListener(new ChangeListener<Boolean>() {
#Override
public void changed(ObservableValue<? extends Boolean> arg0, Boolean arg1, Boolean arg2) {
if (!arg2) {
//commitEdit is replaced with own callback
//commitEdit(getTextArea().getText());
//Update item now since otherwise, it won't get refreshed
setItem(getTextArea().getText());
//Example, provide TableRow and index to get Object of TableView in callback implementation
commitChange.call(new TableCellChangeInfo(getTableRow(), getTableRow().getIndex(), getTextArea().getText()));
}
}
});
...
}
...
}
In cell factory, you just store committed value to the object or do whatever necessary to make it permanent:
col.setCellFactory(new Callback<TableColumn<Object, String>, TableCell<Object, String>>() {
#Override
public TableCell<Object, String> call(TableColumn<Object, String> p) {
return new SimpleEditingTextTableCell(cellChange -> {
TableCellChangeInfo changeInfo = (TableCellChangeInfo)cellChange;
Object obj = myTableView.getItems().get(changeInfo.getRowIndex());
//Save committed value to the object in tableview (and maybe to DB)
obj.field = changeInfo.getChangedObj().toString();
return true;
});
}
});
So far, I have not been able to find any problems with this workaround. On the other hand, I haven't been yet done extensive testing on this either.
EDIT: Well, after some testing noticed, the workaround was working well with big data in tableview but with empty tableview cell was not getting updated after focus lost, only when double clicking it again. There would be ways to refresh table view but that too much hacking for me...
EDIT2: Added setItem(getTextArea().getText()); before calling callback -> works with empty tableview as well.
With reservation of this being a dumb suggestion. Seems too easy. But why don't you just override TableCell#cancelEdit() and save the values manually when it is invoked? When the cell loses focus, cancelEdit() is always invoked to cancel the edit.
class EditableCell extends TableCell<ObservableList<StringProperty>, String> {
private TextField textfield = new TextField();
private int colIndex;
private String originalValue = null;
public EditableCell(int colIndex) {
this.colIndex = colIndex;
textfield.prefHeightProperty().bind(heightProperty().subtract(2.0d));
this.setPadding(new Insets(0));
this.setAlignment(Pos.CENTER);
textfield.setOnAction(e -> {
cancelEdit();
});
textfield.setOnKeyPressed(e -> {
if (e.getCode().equals(KeyCode.ESCAPE)) {
textfield.setText(originalValue);
}
});
}
#Override
public void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if (isEmpty()) {
setText(null);
setGraphic(null);
} else {
if (isEditing()) {
textfield.setText(item);
setGraphic(textfield);
setText(null);
} else {
setText(item);
setGraphic(null);
}
}
}
#Override
public void startEdit() {
super.startEdit();
originalValue = getItem();
textfield.setText(getItem());
setGraphic(textfield);
setText(null);
}
#Override
public void cancelEdit() {
super.cancelEdit();
setGraphic(null);
setText(textfield.getText());
ObservableList<StringProperty> row = getTableView().getItems().get(getIndex());
row.get(colIndex).set(getText());
}
}
I don't know. Maybe I'm missing something. But it seems to work for me.
Update: Added cancel edit functionality. You can now cancel the edit by pressing escape while focusing the text field. Also added so that you can save the edit by pressing enter while focusing the textfield.
I prefer building as much as possible on the existing code, and since this behaviour is still not fixed w/ Java 10, here's a more general approach based on J. Duke's solution from bug: JDK-8089311.
public class TextFieldTableCellAutoCmt<S, T> extends TextFieldTableCell<S, T> {
protected TextField txtFldRef;
protected boolean isEdit;
public TextFieldTableCellAutoCmt() {
this(null);
}
public TextFieldTableCellAutoCmt(final StringConverter<T> conv) {
super(conv);
}
public static <S> Callback<TableColumn<S, String>, TableCell<S, String>> forTableColumn() {
return forTableColumn(new DefaultStringConverter());
}
public static <S, T> Callback<TableColumn<S, T>, TableCell<S, T>> forTableColumn(final StringConverter<T> conv) {
return list -> new TextFieldTableCellAutoCmt<S, T>(conv);
}
#Override
public void startEdit() {
super.startEdit();
isEdit = true;
if (updTxtFldRef()) {
txtFldRef.focusedProperty().addListener(this::onFocusChg);
txtFldRef.setOnKeyPressed(this::onKeyPrs);
}
}
/**
* #return whether {#link #txtFldRef} has been changed
*/
protected boolean updTxtFldRef() {
final Node g = getGraphic();
final boolean isUpd = g != null && txtFldRef != g;
if (isUpd) {
txtFldRef = g instanceof TextField ? (TextField) g : null;
}
return isUpd;
}
#Override
public void commitEdit(final T valNew) {
if (isEditing()) {
super.commitEdit(valNew);
} else {
final TableView<S> tbl = getTableView();
if (tbl != null) {
final TablePosition<S, T> pos = new TablePosition<>(tbl, getTableRow().getIndex(), getTableColumn()); // instead of tbl.getEditingCell()
final CellEditEvent<S, T> ev = new CellEditEvent<>(tbl, pos, TableColumn.editCommitEvent(), valNew);
Event.fireEvent(getTableColumn(), ev);
}
updateItem(valNew, false);
if (tbl != null) {
tbl.edit(-1, null);
}
// TODO ControlUtils.requestFocusOnControlOnlyIfCurrentFocusOwnerIsChild(tbl);
}
}
public void onFocusChg(final ObservableValue<? extends Boolean> obs, final boolean v0, final boolean v1) {
if (isEdit && !v1) {
commitEdit(getConverter().fromString(txtFldRef.getText()));
}
}
protected void onKeyPrs(final KeyEvent e) {
switch (e.getCode()) {
case ESCAPE:
isEdit = false;
cancelEdit(); // see CellUtils#createTextField(...)
e.consume();
break;
case TAB:
if (e.isShiftDown()) {
getTableView().getSelectionModel().selectPrevious();
} else {
getTableView().getSelectionModel().selectNext();
}
e.consume();
break;
case UP:
getTableView().getSelectionModel().selectAboveCell();
e.consume();
break;
case DOWN:
getTableView().getSelectionModel().selectBelowCell();
e.consume();
break;
default:
break;
}
}
}
I found a simple solution to this, one just needs to provide the commit function to column specific to data type:
TableColumn msgstr = new TableColumn("msgstr");
msgstr.setMinWidth(100);
msgstr.prefWidthProperty().bind(widthProperty().divide(3));
msgstr.setCellValueFactory(
new PropertyValueFactory<>("msgstr")
);
msgstr.setOnEditCommit(new EventHandler<CellEditEvent<PoEntry, String>>() {
#Override
public void handle(CellEditEvent<PoEntry, String> t) {
((PoEntry)t.getTableView().getItems().get(t.getTablePosition().getRow())).setMsgstr(t.getNewValue());
}
});
Since TextFieldTableCell is missing functionality (as reckoned in the JDK bug tracker), an alternative solution may work. Forget about TextFieldTableCell and use a custom TableCell class with a TextField in it. The custom TableCell:
public class CommentCell extends TableCell<ListItem, String> {
private final TextField comment = new TextField();
public CommentCell() {
this.comment.setMaxWidth( Integer.MAX_VALUE );
this.comment.setDisable( true );
this.comment.focusedProperty().addListener( new ChangeListener<Boolean>() {
#Override
public void changed( ObservableValue<? extends Boolean> arg0, Boolean oldPropertyValue,
Boolean newPropertyValue ) {
if ( !newPropertyValue ) {
// Binding the TextField text to the model
MainController.getInstance().setComment( getTableRow().getIndex(), comment.getText() );
}
}
} );
this.setGraphic( this.comment );
}
#Override
protected void updateItem( String s, boolean empty ) {
// Checking if the TextField should be editable (based on model condition)
if ( MainController.getInstance().isDependency( getTableRow().getIndex() ) ) {
this.comment.setDisable( false );
this.comment.setEditable( true );
}
// Setting the model value as the text for the TextField
if ( s != null && !s.isEmpty() ) {
this.comment.setText( s );
}
}
}
The UI display might differ from a TextFieldTableCell but at least, it allows for better usability:
UI Display
You'll need:
a CellEditor;
a TableCell or TreeCell subclass; and
a cell factory method.
For more details on the classes shown below, see:
CellEditor
AltTableCell
AltTreeCell
CellEditor
The CellEditor is responsible for handling the Esc and Enter, as well as focus loss:
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.event.EventHandler;
import javafx.scene.Node;
import javafx.scene.control.TableCell;
import javafx.scene.control.TextField;
import javafx.scene.control.TreeCell;
import javafx.scene.input.KeyEvent;
import java.util.function.Consumer;
import static javafx.application.Platform.runLater;
import static javafx.scene.input.KeyCode.ENTER;
import static javafx.scene.input.KeyCode.TAB;
import static javafx.scene.input.KeyEvent.KEY_RELEASED;
public class CellEditor {
private FocusListener mFocusListener;
private final Property<String> mInputText = new SimpleStringProperty();
private final Consumer<String> mConsumer;
/**
* Responsible for accepting the text when users press the Enter or Tab key.
*/
private class KeyHandler implements EventHandler<KeyEvent> {
#Override
public void handle( final KeyEvent event ) {
if( event.getCode() == ENTER || event.getCode() == TAB ) {
commitEdit();
event.consume();
}
}
}
/**
* Responsible for committing edits when focus is lost. This will also
* deselect the input field when focus is gained so that typing text won't
* overwrite the entire existing text.
*/
private class FocusListener implements ChangeListener<Boolean> {
private final TextField mInput;
private FocusListener( final TextField input ) {
mInput = input;
}
#Override
public void changed(
final ObservableValue<? extends Boolean> c,
final Boolean endedFocus, final Boolean beganFocus ) {
if( beganFocus ) {
runLater( mInput::deselect );
}
else if( endedFocus ) {
commitEdit();
}
}
}
/**
* Generalized cell editor suitable for use with {#link TableCell} or
* {#link TreeCell} instances.
*
* #param consumer Converts the field input text to the required
* data type.
* #param graphicProperty Defines the graphical user input field.
*/
public CellEditor(
final Consumer<String> consumer,
final ObjectProperty<Node> graphicProperty ) {
assert consumer != null;
mConsumer = consumer;
init( graphicProperty );
}
private void init( final ObjectProperty<Node> graphicProperty ) {
final var keyHandler = new KeyHandler();
// When the text field is added as the graphics context, we hook into
// the changed value to get a handle on the text field. From there it is
// possible to add change the keyboard and focus behaviours.
graphicProperty.addListener( ( c, o, n ) -> {
if( o instanceof TextField ) {
o.removeEventHandler( KEY_RELEASED, keyHandler );
o.focusedProperty().removeListener( mFocusListener );
}
if( n instanceof final TextField input ) {
n.addEventFilter( KEY_RELEASED, keyHandler );
mInputText.bind( input.textProperty() );
mFocusListener = new FocusListener( input );
n.focusedProperty().addListener( mFocusListener );
}
} );
}
private void commitEdit() {
mConsumer.accept( mInputText.getValue() );
}
}
AltTableCell
The only difference between an AltTableCell and an AltTreeCell is the inheritance hierarchy. They are otherwise identical:
public class AltTableCell<S, T> extends TextFieldTableCell<S, T> {
public AltTableCell( final StringConverter<T> converter ) {
super( converter );
assert converter != null;
new CellEditor(
input -> commitEdit( getConverter().fromString( input ) ),
graphicProperty()
);
}
}
To be concrete, the AltTreeCell would begin:
public class AltTreeCell<T> extends TextFieldTreeCell<T>
Cell factory method
Assign the alternate table cell to the table column's cell factory:
final var column = new TableColumn<Entry<K, V>, T>( label );
column.setEditable( true );
column.setCellFactory(
tableColumn -> new AltTableCell<>(
new StringConverter<>() {
#Override
public String toString( final T object ) {
return object.toString();
}
#Override
public T fromString( final String string ) {
return (T) string;
}
}
)
);
For a tree cell, it's fairly similar:
final var view = new TreeView<>(); // ...
view.setEditable( true );
view.setCellFactory( treeView -> new AltTreeCell<>( converter ) );
Is there any simple approach to let the TreeTableView (or TableView) try to commit values on focus lost?
Unfortunatly I didn't succed with any default-implementations of javafx TableCellFactories, which is why I tried my own TreeTableCell implementations and also some different tableCell implementations like the one from Graham Smith, which seemed the most straight forward, since it already implemented a hook for focus lost, but nevertheless the value is never committed and the userchanges are resetted to the original value.
My guess is, whenever focus is lost, the editingProperty of the affected Cell is always already false which causes the Cell never to commit a value on focusLost. Here the relevant part from the original (oracle-)TreeTableCell Implementation (8u20ea), which causes my approaches to fail:
#Override public void commitEdit(T newValue) {
if (! isEditing()) return; // <-- here my approaches are blocked, because on focus lost its not editing anymore.
final TreeTableView<S> table = getTreeTableView();
if (table != null) {
#SuppressWarnings("unchecked")
TreeTablePosition<S,T> editingCell = (TreeTablePosition<S,T>) table.getEditingCell();
// Inform the TableView of the edit being ready to be committed.
CellEditEvent<S,T> editEvent = new CellEditEvent<S,T>(
table,
editingCell,
TreeTableColumn.<S,T>editCommitEvent(),
newValue
);
Event.fireEvent(getTableColumn(), editEvent);
}
// inform parent classes of the commit, so that they can switch us
// out of the editing state.
// This MUST come before the updateItem call below, otherwise it will
// call cancelEdit(), resulting in both commit and cancel events being
// fired (as identified in RT-29650)
super.commitEdit(newValue);
// update the item within this cell, so that it represents the new value
updateItem(newValue, false);
if (table != null) {
// reset the editing cell on the TableView
table.edit(-1, null);
// request focus back onto the table, only if the current focus
// owner has the table as a parent (otherwise the user might have
// clicked out of the table entirely and given focus to something else.
// It would be rude of us to request it back again.
ControlUtils.requestFocusOnControlOnlyIfCurrentFocusOwnerIsChild(table);
}
}
I succeded with overriding this method and commiting the value "by hand" before the original commitEdit() method is called, but this causes the commit on keys like enter to commit the value twice (on key + on focus lost). Moreover I dont really like my approach at all, so I wonder, if anyone else has solved this in a "nicer" way?
After some digging, turned out that the culprit (aka: the collaborator that cancels the edit before the textField looses focus) is the TableCellBehaviour/Base in its processing of a mousePressed:
mousePressed calls simpleSelect(..)
on detecting a single click it calls edit(-1, null)
which calls the same method on TableView
which sets its editingCell property to null
a tableCell listens to that property and reacts by canceling its own edit
Unfortunately, a hackaround requires 3 collaborators
a TableView with additional api to terminate an edit
a TableCellBehaviour with overridden simpleSelect(...) that calls the additional api (instead of edit(-1..)) before calling super
a TableCell that is configured with the extended behaviour and is aware of table's extended properties
Some code snippets (full code) :
// on XTableView:
public void terminateEdit() {
if (!isEditing()) return;
// terminatingCell is a property that supporting TableCells can listen to
setTerminatingCell(getEditingCell());
if (isEditing()) throw new IllegalStateException(
"expected editing to be terminated but was " + getEditingCell());
setTerminatingCell(null);
}
// on XTableCellBehaviour: override simpleSelect
#Override
protected void simpleSelect(MouseEvent e) {
TableCell<S, T> cell = getControl();
TableView<S> table = cell.getTableColumn().getTableView();
if (table instanceof XTableView) {
((XTableView<S>) table).terminateEdit();
}
super.simpleSelect(e);
}
// on XTextFieldTableCell - this method is called from listener
// to table's terminatingCell property
protected void terminateEdit(TablePosition<S, ?> newPosition) {
if (!isEditing() || !match(newPosition)) return;
commitEdit();
}
protected void commitEdit() {
T edited = getConverter().fromString(myTextField.getText());
commitEdit(edited);
}
/**
* Implemented to create XTableCellSkin which supports terminating edits.
*/
#Override
protected Skin<?> createDefaultSkin() {
return new XTableCellSkin<S, T>(this);
}
Note: the implementation of TableCellBehaviour changed massively between jdk8u5 and jdk8u20 (joys of hacking - not fit for production use ;-) - the method to override in the latter is handleClicks(..)
BTW: massive votingfor JDK-8089514 (was RT-18492 in old jira) might speed up a core fix. Unfortunately, at least the author role is needed to vote/comment bugs in the new tracker.
I also needed this functionality and did some study. I faced some stability issues with XTableView hacking mentioned above.
As problem seems to be commitEdit() won't take effect when focus is lost, why you don't just call your own commit callback from TableCell as follows:
public class SimpleEditingTextTableCell extends TableCell {
private TextArea textArea;
Callback commitChange;
public SimpleEditingTextTableCell(Callback commitChange) {
this.commitChange = commitChange;
}
#Override
public void startEdit() {
...
getTextArea().focusedProperty().addListener(new ChangeListener<Boolean>() {
#Override
public void changed(ObservableValue<? extends Boolean> arg0, Boolean arg1, Boolean arg2) {
if (!arg2) {
//commitEdit is replaced with own callback
//commitEdit(getTextArea().getText());
//Update item now since otherwise, it won't get refreshed
setItem(getTextArea().getText());
//Example, provide TableRow and index to get Object of TableView in callback implementation
commitChange.call(new TableCellChangeInfo(getTableRow(), getTableRow().getIndex(), getTextArea().getText()));
}
}
});
...
}
...
}
In cell factory, you just store committed value to the object or do whatever necessary to make it permanent:
col.setCellFactory(new Callback<TableColumn<Object, String>, TableCell<Object, String>>() {
#Override
public TableCell<Object, String> call(TableColumn<Object, String> p) {
return new SimpleEditingTextTableCell(cellChange -> {
TableCellChangeInfo changeInfo = (TableCellChangeInfo)cellChange;
Object obj = myTableView.getItems().get(changeInfo.getRowIndex());
//Save committed value to the object in tableview (and maybe to DB)
obj.field = changeInfo.getChangedObj().toString();
return true;
});
}
});
So far, I have not been able to find any problems with this workaround. On the other hand, I haven't been yet done extensive testing on this either.
EDIT: Well, after some testing noticed, the workaround was working well with big data in tableview but with empty tableview cell was not getting updated after focus lost, only when double clicking it again. There would be ways to refresh table view but that too much hacking for me...
EDIT2: Added setItem(getTextArea().getText()); before calling callback -> works with empty tableview as well.
With reservation of this being a dumb suggestion. Seems too easy. But why don't you just override TableCell#cancelEdit() and save the values manually when it is invoked? When the cell loses focus, cancelEdit() is always invoked to cancel the edit.
class EditableCell extends TableCell<ObservableList<StringProperty>, String> {
private TextField textfield = new TextField();
private int colIndex;
private String originalValue = null;
public EditableCell(int colIndex) {
this.colIndex = colIndex;
textfield.prefHeightProperty().bind(heightProperty().subtract(2.0d));
this.setPadding(new Insets(0));
this.setAlignment(Pos.CENTER);
textfield.setOnAction(e -> {
cancelEdit();
});
textfield.setOnKeyPressed(e -> {
if (e.getCode().equals(KeyCode.ESCAPE)) {
textfield.setText(originalValue);
}
});
}
#Override
public void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if (isEmpty()) {
setText(null);
setGraphic(null);
} else {
if (isEditing()) {
textfield.setText(item);
setGraphic(textfield);
setText(null);
} else {
setText(item);
setGraphic(null);
}
}
}
#Override
public void startEdit() {
super.startEdit();
originalValue = getItem();
textfield.setText(getItem());
setGraphic(textfield);
setText(null);
}
#Override
public void cancelEdit() {
super.cancelEdit();
setGraphic(null);
setText(textfield.getText());
ObservableList<StringProperty> row = getTableView().getItems().get(getIndex());
row.get(colIndex).set(getText());
}
}
I don't know. Maybe I'm missing something. But it seems to work for me.
Update: Added cancel edit functionality. You can now cancel the edit by pressing escape while focusing the text field. Also added so that you can save the edit by pressing enter while focusing the textfield.
I prefer building as much as possible on the existing code, and since this behaviour is still not fixed w/ Java 10, here's a more general approach based on J. Duke's solution from bug: JDK-8089311.
public class TextFieldTableCellAutoCmt<S, T> extends TextFieldTableCell<S, T> {
protected TextField txtFldRef;
protected boolean isEdit;
public TextFieldTableCellAutoCmt() {
this(null);
}
public TextFieldTableCellAutoCmt(final StringConverter<T> conv) {
super(conv);
}
public static <S> Callback<TableColumn<S, String>, TableCell<S, String>> forTableColumn() {
return forTableColumn(new DefaultStringConverter());
}
public static <S, T> Callback<TableColumn<S, T>, TableCell<S, T>> forTableColumn(final StringConverter<T> conv) {
return list -> new TextFieldTableCellAutoCmt<S, T>(conv);
}
#Override
public void startEdit() {
super.startEdit();
isEdit = true;
if (updTxtFldRef()) {
txtFldRef.focusedProperty().addListener(this::onFocusChg);
txtFldRef.setOnKeyPressed(this::onKeyPrs);
}
}
/**
* #return whether {#link #txtFldRef} has been changed
*/
protected boolean updTxtFldRef() {
final Node g = getGraphic();
final boolean isUpd = g != null && txtFldRef != g;
if (isUpd) {
txtFldRef = g instanceof TextField ? (TextField) g : null;
}
return isUpd;
}
#Override
public void commitEdit(final T valNew) {
if (isEditing()) {
super.commitEdit(valNew);
} else {
final TableView<S> tbl = getTableView();
if (tbl != null) {
final TablePosition<S, T> pos = new TablePosition<>(tbl, getTableRow().getIndex(), getTableColumn()); // instead of tbl.getEditingCell()
final CellEditEvent<S, T> ev = new CellEditEvent<>(tbl, pos, TableColumn.editCommitEvent(), valNew);
Event.fireEvent(getTableColumn(), ev);
}
updateItem(valNew, false);
if (tbl != null) {
tbl.edit(-1, null);
}
// TODO ControlUtils.requestFocusOnControlOnlyIfCurrentFocusOwnerIsChild(tbl);
}
}
public void onFocusChg(final ObservableValue<? extends Boolean> obs, final boolean v0, final boolean v1) {
if (isEdit && !v1) {
commitEdit(getConverter().fromString(txtFldRef.getText()));
}
}
protected void onKeyPrs(final KeyEvent e) {
switch (e.getCode()) {
case ESCAPE:
isEdit = false;
cancelEdit(); // see CellUtils#createTextField(...)
e.consume();
break;
case TAB:
if (e.isShiftDown()) {
getTableView().getSelectionModel().selectPrevious();
} else {
getTableView().getSelectionModel().selectNext();
}
e.consume();
break;
case UP:
getTableView().getSelectionModel().selectAboveCell();
e.consume();
break;
case DOWN:
getTableView().getSelectionModel().selectBelowCell();
e.consume();
break;
default:
break;
}
}
}
I found a simple solution to this, one just needs to provide the commit function to column specific to data type:
TableColumn msgstr = new TableColumn("msgstr");
msgstr.setMinWidth(100);
msgstr.prefWidthProperty().bind(widthProperty().divide(3));
msgstr.setCellValueFactory(
new PropertyValueFactory<>("msgstr")
);
msgstr.setOnEditCommit(new EventHandler<CellEditEvent<PoEntry, String>>() {
#Override
public void handle(CellEditEvent<PoEntry, String> t) {
((PoEntry)t.getTableView().getItems().get(t.getTablePosition().getRow())).setMsgstr(t.getNewValue());
}
});
Since TextFieldTableCell is missing functionality (as reckoned in the JDK bug tracker), an alternative solution may work. Forget about TextFieldTableCell and use a custom TableCell class with a TextField in it. The custom TableCell:
public class CommentCell extends TableCell<ListItem, String> {
private final TextField comment = new TextField();
public CommentCell() {
this.comment.setMaxWidth( Integer.MAX_VALUE );
this.comment.setDisable( true );
this.comment.focusedProperty().addListener( new ChangeListener<Boolean>() {
#Override
public void changed( ObservableValue<? extends Boolean> arg0, Boolean oldPropertyValue,
Boolean newPropertyValue ) {
if ( !newPropertyValue ) {
// Binding the TextField text to the model
MainController.getInstance().setComment( getTableRow().getIndex(), comment.getText() );
}
}
} );
this.setGraphic( this.comment );
}
#Override
protected void updateItem( String s, boolean empty ) {
// Checking if the TextField should be editable (based on model condition)
if ( MainController.getInstance().isDependency( getTableRow().getIndex() ) ) {
this.comment.setDisable( false );
this.comment.setEditable( true );
}
// Setting the model value as the text for the TextField
if ( s != null && !s.isEmpty() ) {
this.comment.setText( s );
}
}
}
The UI display might differ from a TextFieldTableCell but at least, it allows for better usability:
UI Display
You'll need:
a CellEditor;
a TableCell or TreeCell subclass; and
a cell factory method.
For more details on the classes shown below, see:
CellEditor
AltTableCell
AltTreeCell
CellEditor
The CellEditor is responsible for handling the Esc and Enter, as well as focus loss:
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.event.EventHandler;
import javafx.scene.Node;
import javafx.scene.control.TableCell;
import javafx.scene.control.TextField;
import javafx.scene.control.TreeCell;
import javafx.scene.input.KeyEvent;
import java.util.function.Consumer;
import static javafx.application.Platform.runLater;
import static javafx.scene.input.KeyCode.ENTER;
import static javafx.scene.input.KeyCode.TAB;
import static javafx.scene.input.KeyEvent.KEY_RELEASED;
public class CellEditor {
private FocusListener mFocusListener;
private final Property<String> mInputText = new SimpleStringProperty();
private final Consumer<String> mConsumer;
/**
* Responsible for accepting the text when users press the Enter or Tab key.
*/
private class KeyHandler implements EventHandler<KeyEvent> {
#Override
public void handle( final KeyEvent event ) {
if( event.getCode() == ENTER || event.getCode() == TAB ) {
commitEdit();
event.consume();
}
}
}
/**
* Responsible for committing edits when focus is lost. This will also
* deselect the input field when focus is gained so that typing text won't
* overwrite the entire existing text.
*/
private class FocusListener implements ChangeListener<Boolean> {
private final TextField mInput;
private FocusListener( final TextField input ) {
mInput = input;
}
#Override
public void changed(
final ObservableValue<? extends Boolean> c,
final Boolean endedFocus, final Boolean beganFocus ) {
if( beganFocus ) {
runLater( mInput::deselect );
}
else if( endedFocus ) {
commitEdit();
}
}
}
/**
* Generalized cell editor suitable for use with {#link TableCell} or
* {#link TreeCell} instances.
*
* #param consumer Converts the field input text to the required
* data type.
* #param graphicProperty Defines the graphical user input field.
*/
public CellEditor(
final Consumer<String> consumer,
final ObjectProperty<Node> graphicProperty ) {
assert consumer != null;
mConsumer = consumer;
init( graphicProperty );
}
private void init( final ObjectProperty<Node> graphicProperty ) {
final var keyHandler = new KeyHandler();
// When the text field is added as the graphics context, we hook into
// the changed value to get a handle on the text field. From there it is
// possible to add change the keyboard and focus behaviours.
graphicProperty.addListener( ( c, o, n ) -> {
if( o instanceof TextField ) {
o.removeEventHandler( KEY_RELEASED, keyHandler );
o.focusedProperty().removeListener( mFocusListener );
}
if( n instanceof final TextField input ) {
n.addEventFilter( KEY_RELEASED, keyHandler );
mInputText.bind( input.textProperty() );
mFocusListener = new FocusListener( input );
n.focusedProperty().addListener( mFocusListener );
}
} );
}
private void commitEdit() {
mConsumer.accept( mInputText.getValue() );
}
}
AltTableCell
The only difference between an AltTableCell and an AltTreeCell is the inheritance hierarchy. They are otherwise identical:
public class AltTableCell<S, T> extends TextFieldTableCell<S, T> {
public AltTableCell( final StringConverter<T> converter ) {
super( converter );
assert converter != null;
new CellEditor(
input -> commitEdit( getConverter().fromString( input ) ),
graphicProperty()
);
}
}
To be concrete, the AltTreeCell would begin:
public class AltTreeCell<T> extends TextFieldTreeCell<T>
Cell factory method
Assign the alternate table cell to the table column's cell factory:
final var column = new TableColumn<Entry<K, V>, T>( label );
column.setEditable( true );
column.setCellFactory(
tableColumn -> new AltTableCell<>(
new StringConverter<>() {
#Override
public String toString( final T object ) {
return object.toString();
}
#Override
public T fromString( final String string ) {
return (T) string;
}
}
)
);
For a tree cell, it's fairly similar:
final var view = new TreeView<>(); // ...
view.setEditable( true );
view.setCellFactory( treeView -> new AltTreeCell<>( converter ) );
I have an Enum SupplierCode:
public enum SupplierCode
{
BG("British Gas"), CNG("Contract Natural Gas"), COR("Corona Energy");
private String value;
SupplierCode(String value)
{
if(value != "")
{
this.value = value;
}
}
// ... toString() and fromString() omitted for brevity
// for editor framework (?)
public String getValue()
{
return value;
}
public void setValue(String value)
{
this.value = value;
}
}
I display it in my editors using a ValueListBox:
#UiField(provided = true)
ValueListBox<SupplierCode> supplierCode = new ValueListBox<SupplierCode>(new AbstractRenderer<SupplierCode>()
{
#Override
public String render(SupplierCode object)
{
return object == null ? "" : object.toString();
}
});
// in the constructor
public ContractEditor()
{
initWidget(uiBinder.createAndBindUi(this));
supplierCode.setAcceptableValues(Arrays.asList(SupplierCode.values()));
}
I have to edit this type a few times in my app so I wanted to make an editor for just this dropdown, called SupplierCodeEditor:
public class SupplierCodeEditor extends Composite implements Editor<SupplierCode>
{
private static SupplierCodeEditorUiBinder uiBinder = GWT.create(SupplierCodeEditorUiBinder.class);
interface SupplierCodeEditorUiBinder extends UiBinder<Widget, SupplierCodeEditor>
{
}
#UiField(provided = true)
ValueListBox<SupplierCode> value = new ValueListBox<SupplierCode>(new AbstractRenderer<SupplierCode>()
{
#Override
public String render(SupplierCode object)
{
return object == null ? "" : object.toString();
}
});
public SupplierCodeEditor()
{
initWidget(uiBinder.createAndBindUi(this));
value.setAcceptableValues(Arrays.asList(SupplierCode.values()));
}
}
However, when I use it, although it renders the list ok with the options, it doesn't select the actual value from the list. I thought having the getValue() and setValue() methods would work but seemingly not.
Does anyone know of a way to put this in one editor file? Then I won't have to repeat the code for the renderer and call setAcceptableValues() every place I want to use it.
Use LeafValueEditor<SupplierCode>:
public class SupplierEditor extends Composite implements LeafValueEditor<SupplierCode> {
interface SupplierEditorUiBinder extends UiBinder<Widget, SupplierEditor> {
}
private static SupplierEditorUiBinder uiBinder = GWT.create(SupplierEditorUiBinder.class);
#UiField(provided = true)
ValueListBox<SupplierCode> codes;
public SupplierEditor() {
codes = new ValueListBox<>(new AbstractRenderer<SupplierCode>() {
#Override
public String render(SupplierCode object) {
return object == null ? "" : object.toString();
}
});
initWidget(uiBinder.createAndBindUi(this));
codes.setAcceptableValues(Arrays.asList(SupplierCode.values()));
}
#Override
public SupplierCode getValue() {
return codes.getValue();
}
#Override
public void setValue(SupplierCode value) {
codes.setValue(value);
}
}
This way, your widget will be easily pluggable in a Editor hierarchy.
And you don't need the get/set methods in your SupplierCode enum.
You have to either:
use #Editor.Path("") on your child ValueListBox
make your SupplierCodeEditor implement LeafValueEditor<SupplierCode>, with delegating getValue and setValue to the ValueListBox
make your SupplierCodeEditor implement IsEditor<LeafValueEditor<SupplierCode>, returning the ValueListBox's asEditor() from your own asEditor().
BTW, you absolutely don't need the getValue and setValue on your enum values.
CompositeCell let us customize the content of a table cell's in GWT using Java. We can put almost any other group of widget within the table's cell and layout them as we want. Problem is that if we used the html tags to define the layout of the CompositeCell as yet another table (see CompositeCell anonymous class implementation bellow) we loose the event handling for the components of the cell :(.
Running the following code, when we click in the buttons of the cell will realize the popup in response of the event handling IF WE COMMENT the CompositeCell anonymous implementation.
I've been debugging CompositeCell.onBrowserEvent(Context, Element, C, NativeEvent, ValueUpdater) because i think that the definition of the cell layout using HTML table tags breaks the event chain within GWT widgets hierarchy but haven't been successful so far.
Remark: both commented and uncommented versions of the code realize the same GUI layout. This example just intent to show that we loose event handling when customizing cell's content.
public class ActionCellTest implements EntryPoint {
private static final String SERVER_ERROR = "An error occurred while " + "attempting to contact the server. Please check your network "
+ "connection and try again.";
private final GreetingServiceAsync greetingService = GWT.create(GreetingService.class);
public void onModuleLoad() {
CellTable<Person> table = new CellTable<ActionCellTest.Person>();
final List<HasCell<Person, ?>> cells = new LinkedList<HasCell<Person, ?>>();
cells.add(new HasCellImpl("first name", new ActionCell.Delegate<Person>() {
#Override
public void execute(Person object) {
Window.alert(object.getFirstName());
}
}));
cells.add(new HasCellImpl("last name", new ActionCell.Delegate<ActionCellTest.Person>() {
#Override
public void execute(Person object) {
Window.alert(object.getLastName());
}
}));
CompositeCell<Person> cell = new CompositeCell<Person>(cells) {
#Override
public void render(Context context, Person value, SafeHtmlBuilder sb) {
sb.appendHtmlConstant("<table><tbody><tr>");
for (HasCell<Person, ?> hasCell : cells) {
render(context, value, sb, hasCell);
}
sb.appendHtmlConstant("</tr></tbody></table>");
}
#Override
protected <X> void render(Context context, Person value, SafeHtmlBuilder sb, HasCell<Person, X> hasCell) {
Cell<X> cell = hasCell.getCell();
sb.appendHtmlConstant("<td>");
cell.render(context, hasCell.getValue(value), sb);
sb.appendHtmlConstant("</td>");
}
#Override
protected Element getContainerElement(Element parent) {
return parent.getFirstChildElement().getFirstChildElement().getFirstChildElement();
}
};
table.addColumn(new TextColumn<ActionCellTest.Person>() {
#Override
public String getValue(ActionCellTest.Person object) {
return object.getFirstName() + " " + object.getLastName();
}
}, "name");
table.addColumn(new Column<Person, Person>(cell) {
#Override
public Person getValue(ActionCellTest.Person object) {
return object;
}
}, "composite");
LinkedList<Person> data = new LinkedList<ActionCellTest.Person>();
data.add(new Person("Amy", "Reed"));
data.add(new Person("Tim", "Gardner"));
table.setRowData(data);
RootPanel.get().add(table);
}
private class HasCellImpl implements HasCell<Person, Person> {
private ActionCell<Person> fCell;
public HasCellImpl(String text, Delegate<Person> delegate) {
fCell = new ActionCell<Person>(text, delegate);
}
#Override
public Cell<Person> getCell() {
return fCell;
}
#Override
public FieldUpdater<Person, Person> getFieldUpdater() {
return null;
}
#Override
public Person getValue(Person object) {
return object;
}
}
private class Person {
private String fFirstName;
private String fLastName;
public Person(String first, String last) {
fFirstName = first;
fLastName = last;
}
public String getFirstName() {
return fFirstName;
}
public String getLastName() {
return fLastName;
}
}
}
This is a known issue which will be fixed in the upcoming GWT 2.5 (a matter of weeks): http://code.google.com/p/google-web-toolkit/issues/detail?id=5714
(in the mean time, you can run off trunk or try backporting the change)