¿Cómo habilitar commit en focusLost para TableView / TreeTableView?

¿Hay algún enfoque simple para permitir que TreeTableView (o TableView) intente confirmar valores en el foco perdido?

Desafortunadamente, no tuve éxito con ninguna implementación predeterminada de javafx TableCellFactories, por lo que probé mis propias implementaciones TreeTableCell y también algunas implementaciones diferentes de tableCell como la de Graham Smith , que parecía ser la más sencilla, ya que ya implementaba un enganche para el foco perdido, pero sin embargo el valor nunca se confirma y los cambios de usuario se restablecen al valor original.

Supongo que cada vez que se pierde el foco, la propiedad de edición de la celda afectada siempre es falsa, lo que hace que la celda nunca confiera un valor en focusLost. Aquí la parte relevante de la implementación original (oracle-) TreeTableCell (8u20ea), que hace que mis enfoques fallen:

@Override public void commitEdit(T newValue) { if (! isEditing()) return; // <-- here my approaches are blocked, because on focus lost its not editing anymore. final TreeTableView table = getTreeTableView(); if (table != null) { @SuppressWarnings("unchecked") TreeTablePosition editingCell = (TreeTablePosition) table.getEditingCell(); // Inform the TableView of the edit being ready to be committed. CellEditEvent editEvent = new CellEditEvent( table, editingCell, TreeTableColumn.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); } } 

Tuve éxito al anular este método y comprometer el valor “a mano” antes de que se llame al método commitEdit () original, pero esto hace que la confirmación en claves como “ingresar” comprometa el valor dos veces (en la tecla + en el foco perdido). Además, realmente no me gusta mi enfoque, así que me pregunto si alguien más ha resuelto esto de una manera “más agradable”.

Después de algunas excavaciones, resultó que el culpable (también conocido como: el colaborador que cancela la edición antes de que textField pierda el foco) es el TableCellBehaviour / Base en su procesamiento de un mousePressed:

  • mousePressed calls simpleSelect(..)
  • al detectar un solo clic, llama a edit(-1, null)
  • que llama al mismo método en TableView
  • que establece su propiedad editingCell en null
  • un tableCell escucha esa propiedad y reactjs cancelando su propia edición

Desafortunadamente, un hackaround requiere 3 colaboradores

  • una TableView con api adicional para terminar una edición
  • un TableCellBehaviour con reemplazado simpleSelect(...) que llama a la API adicional (en lugar de editar (-1 ..)) antes de llamar a super
  • una TableCell que está configurada con el comportamiento extendido y conoce las propiedades extendidas de la tabla

Algunos fragmentos de código ( código completo ):

 // 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 cell = getControl(); TableView table = cell.getTableColumn().getTableView(); if (table instanceof XTableView) { ((XTableView) table).terminateEdit(); } super.simpleSelect(e); } // on XTextFieldTableCell - this method is called from listener // to table's terminatingCell property protected void terminateEdit(TablePosition 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(this); } 

Nota: la implementación de TableCellBehaviour cambió masivamente entre jdk8u5 y jdk8u20 (alegrías de la piratería – no apto para el uso de producción 😉 – el método para anular en este último es handleClicks(..)

Por cierto: la votación masiva para JDK-8089514 (era RT-18492 en jira antiguo) podría acelerar una solución básica. Lamentablemente, al menos se necesita el rol de autor para votar / comentar errores en el nuevo rastreador.

También necesitaba esta funcionalidad e hice un estudio. Me enfrenté a algunos problemas de estabilidad con el pirateo de XTableView mencionado anteriormente.

Como el problema parece ser commitEdit () no tendrá efecto cuando se pierda el foco, por lo que no solo llama a su propia solicitud de callback desde TableCell de la siguiente manera:

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

En la fábrica de células, solo almacena el valor comprometido del objeto o hace lo que sea necesario para hacerlo permanente:

 col.setCellFactory(new Callback, TableCell>() { @Override public TableCell call(TableColumn 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; }); } }); 

Hasta ahora, no he podido encontrar ningún problema con esta solución. Por otro lado, tampoco he realizado pruebas exhaustivas sobre esto.

EDITAR: Bueno, después de que se notaron algunas pruebas, la solución temporal funcionaba bien con los datos grandes en la tabla vista, pero con la tabla vacía la celda no se actualizaba después de perder el foco, solo cuando se hacía doble clic de nuevo. Habría formas de refrescar la vista de tabla, pero ese exceso de piratería para mí …

EDIT2: agregado setItem (getTextArea (). GetText ()); antes de llamar a callback -> funciona también con tabla vacía.

Con la reserva de esto siendo una sugerencia tonta. Parece muy fácil. Pero ¿por qué no anula TableCell#cancelEdit() y guarda los valores manualmente cuando se invoca? Cuando la celda pierde el foco, cancelEdit() siempre se invoca para cancelar la edición.

 class EditableCell extends TableCell, 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 row = getTableView().getItems().get(getIndex()); row.get(colIndex).set(getText()); } } 

No lo sé. Tal vez me estoy perdiendo algo. Pero parece funcionar para mí.

Actualización: se agregó la funcionalidad de edición de cancelación. Ahora puede cancelar la edición presionando escape mientras enfoca el campo de texto. También se agregó para que pueda guardar la edición al presionar enter mientras enfoca el campo de texto.

Dado que TextFieldTableCell sufre una pérdida importante de funciones (como se calcula en https://bugs.openjdk.java.net/browse/JDK-8089514 ), que está planificado para la reparación en Java 9, decidí optar por una solución alternativa. Por favor, acepte mis disculpas si esto no es el objective, pero aquí está:

La idea principal es olvidar TextFieldTableCell y usar una clase TableCell personalizada con un TextField en ella.

El TableCell personalizado:

 public class CommentCell extends TableCell { private final TextField comment = new TextField(); public CommentCell() { this.comment.setMaxWidth( Integer.MAX_VALUE ); this.comment.setDisable( true ); this.comment.focusedProperty().addListener( new ChangeListener() { @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 ); } } } 

La pantalla de la interfaz de usuario puede diferir de una TextFieldTableCell pero, al menos, permite una mejor usabilidad: UI Display

Encontré una solución simple para esto, solo se necesita proporcionar la función de confirmación a la columna específica para el tipo de datos:

 TableColumn msgstr = new TableColumn("msgstr"); msgstr.setMinWidth(100); msgstr.prefWidthProperty().bind(widthProperty().divide(3)); msgstr.setCellValueFactory( new PropertyValueFactory<>("msgstr") ); msgstr.setOnEditCommit(new EventHandler>() { @Override public void handle(CellEditEvent t) { ((PoEntry)t.getTableView().getItems().get(t.getTablePosition().getRow())).setMsgstr(t.getNewValue()); } }); 

Prefiero construir tanto como sea posible en el código existente, y dado que este comportamiento aún no se soluciona con Java 10, aquí hay un enfoque más general basado en la solución de J. Duke del error: JDK-8089311 .

 public class TextFieldTableCellAutoCmt extends TextFieldTableCell { protected TextField txtFldRef; protected boolean isEdit; public TextFieldTableCellAutoCmt() { this(null); } public TextFieldTableCellAutoCmt(final StringConverter conv) { super(conv); } public static  Callback, TableCell> forTableColumn() { return forTableColumn(new DefaultStringConverter()); } public static  Callback, TableCell> forTableColumn(final StringConverter conv) { return list -> new TextFieldTableCellAutoCmt(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 tbl = getTableView(); if (tbl != null) { final TablePosition pos = new TablePosition<>(tbl, getTableRow().getIndex(), getTableColumn()); // instead of tbl.getEditingCell() final CellEditEvent 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; } } }