How to enable commit on focusLost for TableView/TreeTableView? - java

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 ) );

Related

Is there a way to change a Java property without firing a value changed event to it's listeners?

What I'm trying to do
I'm looking for a way to change a property, without a call to the listeners's changed method.
More specifically I'm trying to implement an undo/redo functionality. The way I've implemented it is as following, in an example with a BooleanProperty and a JavaFX CheckBox.
The selectedProperty of the CheckBox is changed by a mouse click.
A BooleanProperty (actually a JavaFX SimpleBooleanProperty) is changed because it is bound bidirectionally to the selectedProperty
The ChangeListener of the BooleanProperty registers this and adds a Command on the application's undoStack. The Command stores the property, the old and the new value.
The user clicks the undo button
Via the button the application takes that last Command from the stack and calls it's undo() method.
The undo() method changes the BooleanProperty back.
The ChangeListener registers this change again and creates a new Command
An endless cycle is created
My Hacky Solution
The way I did it is by passing the ChangeListener to the Command object. Then the undo() method first removes the ChangeListener, changes the BooleanProperty and then adds the ChangeListener again.
It feels wrong and hacky to pass the ChangeListener to the Command (in my actual implementation in the 3. step there are actually a few more classes between the ChangeListener and the Command which now all need to know about the ChangeListener)
My Question
Is this really the way to do it? Isn't there a way to change the property in step 6 and just tell it to not inform it's listeners? Or at least to get it's listeners?
There's no supported way of bypassing listeners, as you describe. You just need to build this logic into your undo/redo mechanism. The idea is basically to set a flag if you are performing an undo/redo, and not add the change to your stack if so.
Here's a very simple example: note this is not production quality - for example typing in a text control will add to the stack for every character change (keeping copies of the current text at each change). In real code, you should coalesce these changes together.
import java.util.Deque;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import javafx.beans.property.Property;
import javafx.beans.value.ChangeListener;
public class UndoManager {
private boolean performingUndoRedo = false ;
private Deque<Command<?>> undoStack = new LinkedList<>();
private Deque<Command<?>> redoStack = new LinkedList<>();
private Map<Property<?>, ChangeListener<?>> listeners = new HashMap<>();
public <T> void register(Property<T> property) {
// don't register properties multiple times:
if (listeners.containsKey(property)) {
return ;
}
// FIXME: should coalesce (some) changes on the same property, so, e.g. typing in a text
// control does not result in a separate command for each character
ChangeListener<? super T> listener = (obs, oldValue, newValue) -> {
if (! performingUndoRedo) {
Command<T> cmd = new Command<>(property, oldValue, newValue) ;
undoStack.addFirst(cmd);
}
};
property.addListener(listener);
listeners.put(property, listener);
}
public <T> void unregister(Property<T> property) {
listeners.remove(property);
}
public void undo() {
if (undoStack.isEmpty()) {
return ;
}
Command<?> command = undoStack.pop();
performingUndoRedo = true ;
command.undo();
redoStack.addFirst(command);
performingUndoRedo = false ;
}
public void redo() {
if (redoStack.isEmpty()) {
return ;
}
Command<?> command = redoStack.pop();
performingUndoRedo = true ;
command.redo();
undoStack.addFirst(command);
performingUndoRedo = false ;
}
private static class Command<T> {
private final Property<T> property ;
private final T oldValue ;
private final T newValue ;
public Command(Property<T> property, T oldValue, T newValue) {
super();
this.property = property;
this.oldValue = oldValue;
this.newValue = newValue;
}
private void undo() {
property.setValue(oldValue);
}
private void redo() {
property.setValue(newValue);
}
#Override
public String toString() {
return "property: "+property+", from: "+oldValue+", to: "+newValue ;
}
}
}
And here's a quick test harness:
import javafx.application.Application;
import javafx.beans.value.ChangeListener;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ComboBox;
import javafx.scene.control.ListCell;
import javafx.scene.control.TextArea;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
public class UndoExample extends Application {
#Override
public void start(Stage stage) throws Exception {
ComboBox<Color> textColor = new ComboBox<Color>();
textColor.getItems().addAll(Color.BLACK, Color.RED, Color.DARKGREEN, Color.BLUE);
textColor.setValue(Color.BLACK);
textColor.setCellFactory(lv -> new ColorCell());
textColor.setButtonCell(new ColorCell());
CheckBox italic = new CheckBox("Italic");
TextArea text = new TextArea();
updateStyle(text, textColor.getValue(), italic.isSelected());
ChangeListener<Object> listener = (obs, oldValue, newValue) ->
updateStyle(text, textColor.getValue(), italic.isSelected());
textColor.valueProperty().addListener(listener);
italic.selectedProperty().addListener(listener);
UndoManager undoMgr = new UndoManager();
undoMgr.register(textColor.valueProperty());
undoMgr.register(italic.selectedProperty());
undoMgr.register(text.textProperty());
Button undo = new Button("Undo");
Button redo = new Button("Redo");
undo.setOnAction(e -> undoMgr.undo());
redo.setOnAction(e -> undoMgr.redo());
HBox controls = new HBox(textColor, italic, undo, redo);
controls.setSpacing(5);
BorderPane root = new BorderPane(text);
root.setTop(controls);
Scene scene = new Scene(root);
stage.setScene(scene);
stage.show();
}
private void updateStyle(TextArea text, Color textColor, boolean italic) {
StringBuilder style = new StringBuilder()
.append("-fx-text-fill: ")
.append(hexString(textColor))
.append(";")
.append("-fx-font: ");
if (italic) {
style.append("italic ");
}
style.append("13pt sans-serif ;");
text.setStyle(style.toString());
}
private String hexString(Color color) {
int r = (int) (color.getRed() * 255) ;
int g = (int) (color.getGreen() * 255) ;
int b = (int) (color.getBlue() * 255) ;
return String.format("#%02x%02x%02x", r, g, b);
}
private static class ColorCell extends ListCell<Color> {
private Rectangle rect = new Rectangle(25, 25);
#Override
protected void updateItem(Color color, boolean empty) {
super.updateItem(color, empty);
if (empty || color==null) {
setGraphic(null);
} else {
rect.setFill(color);
setGraphic(rect);
}
}
}
public static void main(String[] args) {
Application.launch(args);
}
}
There is pretty much not a possibility to do this without "hacks"!
However, there is also a shorter solution, via using reflection:
/**
* Set the value of property without firing any change event.
* The value of property will be set via reflection.
* This property must be "Base" property such as {#link DoublePropertyBase}.
*
* #param property | Property to set!
* #param newValue | New value of property.
*/
public static <T> void setPropertyWithoutFiringEvent(Property<T> property, T newValue)
{
Class<?> cls = property.getClass();
while (cls != null) //While until helper variable is found
{
try
{
Field fieldH = cls.getDeclaredField("helper"), fieldV = cls.getDeclaredField("valid");
fieldH.setAccessible(true);
fieldV.setAccessible(true);
Object helper = fieldH.get(property), valid = fieldV.getBoolean(property); //Temporary values
fieldH.set(property, null); //Disabling ExpressionHelper by setting it on null;
property.setValue(newValue);
fieldH.set(property, helper); //Setting helper back!
fieldV.set(property, valid); //Important
return;
}
catch (Exception e)
{
cls = cls.getSuperclass(); //If not found go to super class of property next time!
}
}
System.err.println("Property " + property + " cant be set because variable \"helper\" was not found!");
}
This function temporarily disables ExpressionHelper what is an object responsible for firing change events, and then it will change the value of property and enable ExpressionHelper back! This will cause that one change will not be notified!
If the reflection is not friendly solution for you, then just use the solution above however this one is far shorter and simpler.

StyleableProperty: how to change value programatically at runtime?

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;
}
}

JavaFX Display Enum with String field in Combobox as String (in TableView)

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());
});

Javafx: fail to display content properly in table cell

I have the following Trade object class.
public class Trade implements Comparable<Trade>{
// attributes of each trade that go into the tableViewTransaction log
// properties
private StringProperty itemID;
public Trade(int itemID){
this.itemID = new SimpleStringProperty(String.format("%04d",itemID));
}
public String getItemID(){
return this.itemID.get();
}
public StringProperty itemIDProperty(){
return this.itemID;
}
public void setItemID(String itemID){
int id = Integer.parseInt(itemID);
this.itemID.set(String.format("%04d",id));
}
}
Now in my Controller class, I have a tableView TransactionLog and a table column for itemID.
public TableView<Trade> fxTransactionLog;
public TableColumn<Trade, String> fxTransactionLogItemID;
The tableView is editable, so is the table column using the following code.
Here is where the problem is: The tableView is able to display itemID perfectly. For example say when I create a new Trade object with itemID = 1, the table cell will display 0001, then I decide to edit the itemID of a Trade object and type in to a new ID of 13, it will show up as 0013 like below.
0001 -> 0013 // This works fine, if I edit the cell and assign a DIFFERENT value to the cell.
However, if I click to edit the itemID and assign the same value it already has, which is 1 in this case. It displays 1, which is not what I want, as it is missing the leading zeros. I looked through my code and just couldn't figure out why this is happening. This is more of an aesthetic issue.
0001 -> 1 // edit and reassign same value to the cell
How can I make it display 0001 instead of just 1 even if I assign the SAME value to the cell ?
Secondly, what code and where should I write to prevent the user from typing in String for itemID ?
UPDATE: So I followed thislink Example 12-11 . I created a separate class for EditingItemIDCell.
import javafx.scene.control.TableCell;
import javafx.scene.control.TextField;
public class EditingItemIDCell extends TableCell{
private TextField textField;
public EditingItemIDCell() {
}
#Override
public void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setText(null);
setGraphic(null);
} else {
if (isEditing()) {
if (textField != null) {
textField.setText(String.format("%04d",Integer.parseInt(getString())));
}
setText(null);
setGraphic(textField);
} else {
setText(getString());
setGraphic(null);
}
}
}
private String getString() {
return getItem() == null ? "" : getItem().toString();
}
}
And in my Controller class, I made the following changes.
But I am getting an error saying:
The method setCellFactory(Callback<TableColumn<Trade,String>,TableCell<Trade,String>>) in the type TableColumn<Trade,String> is not applicable for the arguments (Callback<TableColumn,TableCell>).
Define cellfactory as;
Callback<TableColumn<Trade, String>, TableCell<Trade, String>> cellFactory
= new Callback<TableColumn<Trade, String>, TableCell<Trade, String>>()
{
public TableCell call( TableColumn<Trade, String> p )
{
return new EditingItemIDCell();
}
};
In EditingItemIDCell when you create textfield do
private void createTextField()
{
NumberFormat nf = NumberFormat.getIntegerInstance();
textField = new TextField();
// add filter to allow for typing only integer
textField.setTextFormatter( new TextFormatter<>( c ->
{
if (c.getControlNewText().isEmpty()) {
return c;
} // for the No.2 issue in the comment
ParsePosition parsePosition = new ParsePosition( 0 );
Object object = nf.parse( c.getControlNewText(), parsePosition );
if ( object == null || parsePosition.getIndex() < c.getControlNewText().length() )
{
return null;
}
else
{
return c;
}
} ) );
textField.setText( getString() );
textField.setMinWidth( this.getWidth() - this.getGraphicTextGap() * 2 );
// commit on Enter
textField.setOnAction( new EventHandler<ActionEvent>()
{
#Override
public void handle( ActionEvent event )
{
commitEdit( textField.getText() );
}
} );
textField.focusedProperty().addListener( new ChangeListener<Boolean>()
{
#Override
public void changed( ObservableValue<? extends Boolean> arg0,
Boolean arg1, Boolean arg2 )
{
if ( !arg2 )
{
commitEdit( textField.getText() );
}
}
} );
}
and finally, your actual problem, since you decided to format the input value in Trade class setters, when the user commits the same value, tableview determines the original value is not changed and do not update the rendered value in a cell, so when user commits "1" again, the underlying value of Trade instance is "0001", but the rendered value remains with "1". The dirty workaround may be to change the value to some arbitrary intermediate one:
public void setItemID( String itemID )
{
this.itemID.set( "0" );
int id = Integer.parseInt( itemID );
this.itemID.set( String.format( "%04d", id ) );
}
Edit
1) I still want the existing textfield 0001 in the tableCell to be
highlighted, when I double click on the cell to edit ?
Change the startEdit to this:
#Override
public void startEdit()
{
if ( !isEmpty() )
{
super.startEdit();
createTextField();
setText( null );
setGraphic( textField );
textField.requestFocus();
// textField.selectAll(); commenting out this because
// JavaFX confuses the caret position described in the comment
// as OP has observed. Seems to be a bug.
}
}
2) Plus, with your existing code, i can never delete the integer on
the first index when editing.
In the textfield filter, check for emptiness with:
if (c.getControlNewText().isEmpty()) {
return c;
}
I also edited the filter code above.

Committing a value in TableView at focus change [duplicate]

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 ) );

Categories

Resources