package com.netthreads.network.osc.router.table.cell; import java.util.ArrayList; import java.util.List; import javafx.application.Platform; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.event.EventHandler; import javafx.scene.control.ContentDisplay; import javafx.scene.control.TableCell; import javafx.scene.control.TableColumn; import javafx.scene.control.TextField; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyEvent; /** * http://www.wobblycogs.co.uk/index.php/computing/javafx/145-editing-null- * data-values-in-a-cell-with-javafx-2 * * Provides the basis for an editable table cell using a text field. Sub-classes * can provide formatters for display and a commitHelper to control when editing * is committed. * * @author Graham Smith */ public abstract class EditableTextTableCell<S, T> extends TableCell<S, T> { protected TextField textField; @Override public void startEdit() { super.startEdit(); if (textField == null) { createTextField(); } setGraphic(textField); setContentDisplay(ContentDisplay.GRAPHIC_ONLY); Platform.runLater(new Runnable() { @Override public void run() { textField.selectAll(); textField.requestFocus(); } }); } @Override public void cancelEdit() { super.cancelEdit(); setText(getString()); setContentDisplay(ContentDisplay.TEXT_ONLY); // Once the edit has been cancelled we no longer need the text field // so we mark it for cleanup here. Note though that you have to handle // this situation in the focus listener which gets fired at the end // of the editing. textField = null; } @Override public void updateItem(T item, boolean empty) { super.updateItem(item, empty); if (empty) { setText(null); setGraphic(null); } else { if (isEditing()) { if (textField != null) { textField.setText(getString()); } setGraphic(textField); setContentDisplay(ContentDisplay.GRAPHIC_ONLY); } else { setText(getString()); setContentDisplay(ContentDisplay.TEXT_ONLY); } } } /** * Create editable text field and hook into handlers. * */ private void createTextField() { textField = new TextField(getString()); textField.setMinWidth(this.getWidth() - this.getGraphicTextGap() * 2); textField.setOnKeyPressed(new EventHandler<KeyEvent>() { @SuppressWarnings( { "rawtypes", "unchecked" }) @Override public void handle(KeyEvent t) { if (t.getCode() == KeyCode.ENTER) { commitHelper(false); } else if (t.getCode() == KeyCode.ESCAPE) { cancelEdit(); } else if (t.getCode() == KeyCode.TAB) { commitHelper(false); TableColumn nextColumn = getNextColumn(!t.isShiftDown()); if (nextColumn != null) { getTableView().edit(getTableRow().getIndex(), nextColumn); } } } }); textField.focusedProperty().addListener(new ChangeListener<Boolean>() { @Override public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) { // This focus listener fires at the end of cell editing when // focus is lost // and when enter is pressed (because that causes the text field // to lose focus). // The problem is that if enter is pressed then cancelEdit is // called before this // listener runs and therefore the text field has been cleaned // up. If the // text field is null we don't commit the edit. This has the // useful side effect // of stopping the double commit. if (!newValue && textField != null) { commitHelper(true); } } }); } /** * Return target column. * * @param forward * true gets the column to the right, false the column to the * left of the current column * * @return Return column. */ private TableColumn<S, ?> getNextColumn(boolean forward) { List<TableColumn<S, ?>> columns = new ArrayList<>(); for (TableColumn<S, ?> column : getTableView().getColumns()) { columns.addAll(getLeaves(column)); } // There is no other column that supports editing. if (columns.size() < 2) { return null; } int currentIndex = columns.indexOf(getTableColumn()); int nextIndex = currentIndex; if (forward) { nextIndex++; if (nextIndex > columns.size() - 1) { nextIndex = 0; } } else { nextIndex--; if (nextIndex < 0) { nextIndex = columns.size() - 1; } } return columns.get(nextIndex); } /** * Return column leaves. * * @param root * * @return the column leaves. */ private List<TableColumn<S, ?>> getLeaves(TableColumn<S, ?> root) { List<TableColumn<S, ?>> columns = new ArrayList<>(); if (root.getColumns().isEmpty()) { // We only want the leaves that are editable. if (root.isEditable()) { columns.add(root); } return columns; } else { for (TableColumn<S, ?> column : root.getColumns()) { columns.addAll(getLeaves(column)); } return columns; } } /** * Return text field for input. * * @return */ public TextField getTextField() { return textField; } /** * Any action attempting to commit an edit should call this method rather * than commit the edit directly itself. This method will perform any * validation and conversion required on the value. For text values that * normally means this method just commits the edit but for numeric values, * for example, it may first parse the given input. * <p> * The only situation that needs to be treated specially is when the field * is losing focus. If you user hits enter to commit the cell with bad data * we can happily cancel the commit and force them to enter a real value. If * they click away from the cell though we want to give them their old value * back. * * @param losingFocus * true if the reason for the call was because the field is * losing focus. */ protected abstract void commitHelper(boolean losingFocus); /** * Provides the string representation of the value of this cell when the * cell is not being edited. */ protected abstract String getString(); }