Skip to content

Instantly share code, notes, and snippets.

@eckig
Last active April 8, 2023 08:07
Show Gist options
  • Star 9 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save eckig/30abf0d7d51b7756c2e7 to your computer and use it in GitHub Desktop.
Save eckig/30abf0d7d51b7756c2e7 to your computer and use it in GitHub Desktop.
import javafx.beans.binding.Bindings;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.control.Cell;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TextArea;
import javafx.scene.input.KeyCode;
import javafx.util.Callback;
import javafx.util.StringConverter;
import javafx.util.converter.DefaultStringConverter;
public class TextAreaTableCell<S, T> extends TableCell<S, T> {
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> converter) {
return list -> new TextAreaTableCell<>(converter);
}
private static <T> String getItemText(Cell<T> cell, StringConverter<T> converter) {
return converter == null ? cell.getItem() == null ? "" : cell.getItem()
.toString() : converter.toString(cell.getItem());
}
private static <T> TextArea createTextArea(final Cell<T> cell, final StringConverter<T> converter) {
TextArea textArea = new TextArea(getItemText(cell, converter));
textArea.setOnKeyReleased(t -> {
if (t.getCode() == KeyCode.ESCAPE) {
cell.cancelEdit();
t.consume();
}
else if(t.getCode() == KeyCode.ENTER && t.isShiftDown()) {
if (converter == null) {
throw new IllegalStateException(
"Attempting to convert text input into Object, but provided "
+ "StringConverter is null. Be sure to set a StringConverter "
+ "in your cell factory.");
}
cell.commitEdit(converter.fromString(textArea.getText()));
t.consume();
}
});
textArea.prefRowCountProperty().bind(Bindings.size(textArea.getParagraphs()));
return textArea;
}
private void startEdit(final Cell<T> cell, final StringConverter<T> converter) {
textArea.setText(getItemText(cell, converter));
cell.setText(null);
cell.setGraphic(textArea);
textArea.selectAll();
textArea.requestFocus();
}
private static <T> void cancelEdit(Cell<T> cell, final StringConverter<T> converter) {
cell.setText(getItemText(cell, converter));
cell.setGraphic(null);
}
private void updateItem(final Cell<T> cell, final StringConverter<T> converter) {
if (cell.isEmpty()) {
cell.setText(null);
cell.setGraphic(null);
} else {
if (cell.isEditing()) {
if (textArea != null) {
textArea.setText(getItemText(cell, converter));
}
cell.setText(null);
cell.setGraphic(textArea);
} else {
cell.setText(getItemText(cell, converter));
cell.setGraphic(null);
}
}
}
private TextArea textArea;
private ObjectProperty<StringConverter<T>> converter = new SimpleObjectProperty<>(this, "converter");
public TextAreaTableCell() {
this(null);
}
public TextAreaTableCell(StringConverter<T> converter) {
this.getStyleClass().add("text-area-table-cell");
setConverter(converter);
}
public final ObjectProperty<StringConverter<T>> converterProperty() {
return converter;
}
public final void setConverter(StringConverter<T> value) {
converterProperty().set(value);
}
public final StringConverter<T> getConverter() {
return converterProperty().get();
}
@Override
public void startEdit() {
if (!isEditable() || !getTableView().isEditable() || !getTableColumn().isEditable()) {
return;
}
super.startEdit();
if (isEditing()) {
if (textArea == null) {
textArea = createTextArea(this, getConverter());
}
startEdit(this, getConverter());
}
}
@Override
public void cancelEdit() {
super.cancelEdit();
cancelEdit(this, getConverter());
}
@Override
public void updateItem(T item, boolean empty) {
super.updateItem(item, empty);
updateItem(this, getConverter());
}
}
@DominikStyp
Copy link

Thanks very much for this implementation - shame that Oracle didn't do this...
I think that "chat version" of the key's would be better - so if you click ENTER you confirm the change, and if you click SHIFT + ENTER you go to new line:

private static <T> TextArea createTextArea(final Cell<T> cell, final StringConverter<T> converter) {
        TextArea textArea = new TextArea(getItemText(cell, converter));
        textArea.setOnKeyReleased(t -> {
            if (t.getCode() == KeyCode.ESCAPE) {
                cell.cancelEdit();
                t.consume();
            }
            else if(t.getCode() == KeyCode.ENTER && t.isShiftDown()) {
                t.consume();
                textArea.appendText("\n");
            }
            else if(t.getCode() == KeyCode.ENTER) {
                if (converter == null) {
                    throw new IllegalStateException(
                        "Attempting to convert text input into Object, but provided "
                            + "StringConverter is null. Be sure to set a StringConverter "
                            + "in your cell factory.");
                }
                cell.commitEdit(converter.fromString(textArea.getText()));
                t.consume();    
            }
        });
        textArea.prefRowCountProperty().bind(Bindings.size(textArea.getParagraphs()));
        return textArea;
    }

@limitedAtonement
Copy link

limitedAtonement commented Jan 10, 2017

Should not be textArea.appendText("\n"), but rather textArea.insertText(textArea.getCaretPosition(), "\n"). With your code, when the caret is within the text, "Enter" adds a new line at the end. It took me a long time to figure this out.

@TurekBot
Copy link

TurekBot commented Aug 2, 2017

I found that in the createTextArea method using textArea.setOnKeyReleased(... let an extra newline get in there before commiting. Using textArea.setOnKeyPressed(... instead fixed that for me. (Also, thanks @limitedAtonement and @DominikStyp for the tips.)

I'm new to Gists—if you can edit them, I can't figure out how—but I do have a fork with the above-mentioned improvements.

@TurekBot
Copy link

An improvement to the text area sizing:

   private void startEdit(final Cell<T> cell, final StringConverter<T> converter) {
        textArea.setText(getItemText(cell, converter));

        cell.setText(null);
        cell.setGraphic(textArea);

        // Make sure the text area stays the right size:
        /* The following solution is courtesy of James_D: https://stackoverflow.com/a/22733264/5432315 */
        // Perform a lookup for an element with a css class of "text"
        // This will give the Node that actually renders the text inside the
        // TextArea
        Node text = textArea.lookup(".text");
        // Bind the preferred height of the text area to the actual height of the text
        // This will make the text area the height of the text, plus some padding
        // of 20 pixels, as long as that height is between the text area's minHeight
        // and maxHeight. The max height will be the height of its parent (usually).
        textArea.prefHeightProperty().bind(Bindings.createDoubleBinding(() ->
                text.getBoundsInLocal().getHeight(), text.boundsInLocalProperty()).add(20)
        );


        textArea.selectAll();
        textArea.requestFocus();
    }

@maroc81
Copy link

maroc81 commented Jan 13, 2023

Thanks for this! I adapted it for a kotlin implementation that also wraps the text in the cell.

https://gist.github.com/maroc81/6c60d5ae11fdf79099e50fe308509a27

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment