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;
}
}
Related
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());
});
I have a simple example of the add-on switch with vaadin, what I want is to keep the state of the switch even when I update the UI, that is, I support multiple tabs, but I can not do it, this push example is very similar to What I want to do but with a textField.
https://github.com/vaadin-marcus/push-example/blob/master/src/main/java/com/vaadin/training/ScrumBoardLayout.java
https://github.com/rucko24/MVP/blob/testingSwitchPushTemu/src/main/java/com/Core/vaadin/pushServer/ejemploPushMarkus/ScrumBoard.java
To my example I add a bulb so that when another accesses the application can see the current state of the bulb. My example in github is this with only 3 classes
https://github.com/rucko24/MVP/tree/testingSwitchPushTemu/src/main/java/com/Core/vaadin/pushServer/ejemploPushMarkus
This is the swithc listener that changes my bulb, but when I get the boolean value (true, or false), I still do not understand the right way to push the other switch
switchTemu.addValueChangeListener(new Property.ValueChangeListener() {
private static final long serialVersionUID = 1L;
#Override
public void valueChange(Property.ValueChangeEvent event) {
boolean estado = (boolean) event.getProperty().getValue();
ScrumBoard.addSwitch(estado);
switchTemu.removeValueChangeListener(this);
if(estado == Boolean.TRUE) {
bombilla.setIcon(bombillaON);
}else {
bombilla.setIcon(bombillaOFF);
}
switchTemu.addValueChangeListener(this);
}
});
Update
In my example github achievement, change the state of all switches to all UI, but I still do not know how to get the state of the switches
I made a couple of changes to your sources (still basic, but it gets you started):
only 1 common shared state
switch value change listeners now just trigger a state changed event
state changed listeners now update the UI elements when triggered
upon registration, a state changed listeners is informed (triggered) about the current state
The main idea is to have just a single shared state and any change is communicated to all the listeners (including the one where the change originated).
Below you can find the code: (P.S. I did not recompile my widgetset so the nice switch icon falls back to the default check box style)
1) SwitchState - represents the state of the switch shared between all the app instances
public enum SwitchState {
ON(true, new ThemeResource("img/on.png")), OFF(false, new ThemeResource("img/off.png"));
private final boolean value;
private final ThemeResource icon;
SwitchState(boolean value, ThemeResource icon) {
this.value = value;
this.icon = icon;
}
public boolean getValue() {
return value;
}
public ThemeResource getIcon() {
return icon;
}
public static SwitchState from(boolean value) {
return value ? ON : OFF;
}
}
2) ScrumBoard common state and listeners manager
public class ScrumBoard {
// list of listeners
private static List<SwitchChangeListener> LISTENERS = new ArrayList<>();
// initial state
private static SwitchState STATE = SwitchState.OFF;
// state change listener contract
public interface SwitchChangeListener {
void handleStateChange(SwitchState state);
}
// handle a a state change request
public static synchronized void updateState(boolean value) {
STATE = SwitchState.from(value);
fireChangeEvent(STATE);
}
// register a new state listener
public static synchronized void addSwitchChangeListener(SwitchChangeListener listener) {
System.out.println("Added listener for " + listener);
LISTENERS.add(listener);
// when a new listener is registered, also inform it of the current state
listener.handleStateChange(STATE);
}
// remove a state listener
public static synchronized void removeSwitchListener(SwitchChangeListener listener) {
LISTENERS.remove(listener);
}
// fire a change event to all registered listeners
private static void fireChangeEvent(SwitchState state) {
for (SwitchChangeListener listener : LISTENERS) {
listener.handleStateChange(state);
}
}
}
3) ScrumBoardLayout - UI layout and components
public class ScrumBoardLayout extends VerticalLayout implements ScrumBoard.SwitchChangeListener {
private Label icon = new Label();
private Switch mySwitch = new Switch();
public ScrumBoardLayout() {
setMargin(true);
setSpacing(true);
addHeader();
// listen for state changes
ScrumBoard.addSwitchChangeListener(this);
}
private void addHeader() {
mySwitch.setImmediate(true);
icon.setSizeUndefined();
// notify of state change
mySwitch.addValueChangeListener((Property.ValueChangeListener) event -> ScrumBoard.updateState((Boolean) event.getProperty().getValue()));
VerticalLayout layout = new VerticalLayout();
layout.setHeight("78%");
layout.addComponents(icon, mySwitch);
layout.setComponentAlignment(icon, Alignment.BOTTOM_CENTER);
layout.setComponentAlignment(mySwitch, Alignment.BOTTOM_CENTER);
layout.setExpandRatio(mySwitch, 1);
addComponents(layout);
}
#Override
public void handleStateChange(SwitchState state) {
// update UI on state change
UI.getCurrent().access(() -> {
mySwitch.setValue(state.getValue());
icon.setIcon(state.getIcon());
Notification.show(state.name(), Type.ASSISTIVE_NOTIFICATION);
});
}
#Override
public void detach() {
super.detach();
ScrumBoard.removeSwitchListener(this);
}
}
4) Result
I could see that with the ThemeResource () class, changing the bulb to its ON / OFF effect is strange, but I solve it as follows
.bombillo-on {
#include valo-animate-in-fade($duration: 1s);
width: 181px;
height: 216px;
background: url(img/on.png) no-repeat;
}
.bombillo-off {
#include valo-animate-in-fade($duration: 1s);
width: 181px;
height: 216px;
background: url(img/off.png) no-repeat;
}
public enum Sstate {
ON(true,"bombillo-on"),
OFF(false,"bombillo-off");
private boolean value;
private String style;
Sstate(boolean value, String style) {
this.value = value;
this.style = style;
}
public boolean getValue() { return value;}
public String getStyle() { return style;}
public static Sstate from(boolean value) { return value ? ON:OFF;}
}
And the handleChangeEvent It stays like this
#Override
public void handleChangeEvent(Sstate state) {
ui.access(() -> {
bombilla.setStyleName(state.getStyle());
s.setValue(state.getValue());
System.out.println(state+" values "+s);
});
}
UPDATE:
I notice an issue, that when I add a new view, or change using the buttonMenuToggle, it loses the synchronization, and update the bulb quite strange, clear with the themeResource does not happen that.
Solution:
to avoid UiDetachedException when using the Navigator try this, It works very well
#Override
public void handleChangeEvent(Sstate state) {
if(!ui.isAttached()) {
BroadcastesSwitch.removeListener(this);
return;
}
ui.access(() -> {
bombilla.setStyleName(state.getStyle());
s.setValue(state.getValue());
System.out.println(state+" values "+s);
});
}
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 am trying to use TextField in javafx.
The scenario: I have list view populated with specific objects and edit button to edit the object associated with list cell of list view.
When I click on edit button it redirects me to a pane with editing feature where I can edit the name of that object and save it using a save button.
So I have to put validation on save button to make it enable and disable.
If I edit the name in text field then it should enable the save button otherwise it should remains disabled.
I have tried using different methods on text fields as below.
textField.textPorperty.addListener(listener -> {
//Logic to enable disable save button
});
As I am using list view, this listener gives me old value as previously edited object which does not satisfy my condition.
I can not use
textField.focusedProperty().addListener((observableValue, oldValue, newValue) -> {});
as It does not give me expected behavior.
Can anyone help me to solve this issue?
You need to implement additional logic that decides whether or not a change to the textProperty should change the enablement state of the button. This requires:
a reference to the initial value (on setting the text to the input, f.i. on changes to selection in the list)
a boolean property that keeps the enablement state (below it's called buffering)
a listener to the textField that updates the enablement state as needed
Below is a very simplified example - just to get you started - that extracts those basics into a dedicated class named BufferedTextInput. Buffering is changed internally on:
set to false if the "subject" value is set or a change is committed/discarded
set to true once on being notified on the first change of the textField
More complex logic (like not buffering on detecting a change back to the original value) can be implemented as needed.
/**
* Bind disable property of commit/cancel button to actual change.
* http://stackoverflow.com/q/29935643/203657
*/
public class ManualBufferingDemo extends Application {
private Parent getContent() {
ObservableList<Person> persons = FXCollections.observableList(Person.persons(),
person -> new Observable[] {person.lastNameProperty()});
ListView<Person> listView = new ListView<>(persons);
TextField lastName = new TextField();
Consumer<String> committer = text -> System.out.println("committing: " + text);
BufferedTextInput buffer = new BufferedTextInput(lastName, committer);
Button save = new Button("Save");
save.setOnAction(e -> {
buffer.commit();
});
save.disableProperty().bind(Bindings.not(buffer.bufferingProperty()));
Button cancel = new Button("Cancel");
cancel.setOnAction(e -> {
buffer.flush();
});
listView.getSelectionModel().selectedItemProperty().addListener((source, old, current) -> {
buffer.setSubject(current.lastNameProperty());
});
cancel.disableProperty().bind(Bindings.not(buffer.bufferingProperty()));
VBox content = new VBox(listView, lastName, save, cancel);
return content;
}
public static class BufferedTextInput {
private ReadOnlyBooleanWrapper buffering;
private StringProperty value;
private TextField input;
private Consumer<String> committer;
public BufferedTextInput(TextField input, Consumer<String> committer) {
buffering = new ReadOnlyBooleanWrapper(this, "buffering", false);
value = new SimpleStringProperty(this, "");
this.input = input;
this.committer = committer;
input.textProperty().addListener((source, old, current) -> {
updateState(old, current);
});
input.setOnAction(e -> commit());
}
private void updateState(String old, String current) {
if (isBuffering()) return;
if (value.get().equals(current)) return;
setBuffering(true);
}
public void setSubject(StringProperty value) {
this.value = value;
input.setText(value.get());
setBuffering(false);
}
public void commit() {
committer.accept(input.getText());
this.value.set(input.getText());
setBuffering(false);
}
public void flush() {
input.setText(value.get());
setBuffering(false);
}
public boolean isBuffering() {
return buffering.get();
}
public ReadOnlyBooleanProperty bufferingProperty() {
return buffering.getReadOnlyProperty();
}
private void setBuffering(boolean buffer) {
buffering.set(buffer);
}
}
#Override
public void start(Stage primaryStage) throws Exception {
primaryStage.setScene(new Scene(getContent()));
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
For production use, such direct coupling between view and model (f.i. when needing the buffering for a complete form) isn't good enough, further separation might be needed. See BufferedObjectProperty and its usage in a FX adaption of the infamous AlbumManager example (very crude)
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 ) );