Skip to content

Instantly share code, notes, and snippets.

@TurekBot
Forked from eckig/TextAreaTableCell.java
Last active November 23, 2020 16:59
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save TurekBot/69495ba2b15771c8118f4be1ec722381 to your computer and use it in GitHub Desktop.
Save TurekBot/69495ba2b15771c8118f4be1ec722381 to your computer and use it in GitHub Desktop.
An attempt to improve the already great TextAreaTableCell, see Stack Overflow question: http://stackoverflow.com/questions/27666382/editable-multiline-cell-in-tableview
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;
/**
* Creates a TableCell that contains a TextArea similar to a TextFieldTableCell.
* <p>
* Originally created by eckig: https://gist.github.com/eckig/30abf0d7d51b7756c2e7
* <p>
* Adapted from the original TextAreaTableCell by Brad Turek.
* @param <S>
* @param <T>
*/
@SuppressWarnings("WeakerAccess")
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.setWrapText(true);
textArea.setOnKeyPressed(t -> {
if (t.getCode() == KeyCode.ESCAPE) {
cell.cancelEdit();
t.consume();
}
else if(t.getCode() == KeyCode.ENTER && t.isShiftDown()) {
t.consume();
textArea.insertText(textArea.getCaretPosition(), "\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();
}
});
return textArea;
}
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();
}
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);
cell.setTooltip(null);
} else {
if (cell.isEditing()) {
if (textArea != null) {
textArea.setText(getItemText(cell, converter));
}
cell.setText(null);
cell.setGraphic(textArea);
cell.setTooltip(null);
} else {
cell.setText(getItemText(cell, converter));
cell.setGraphic(null);
//Add text as tooltip so that user can read text without editing it.
Tooltip tooltip = new Tooltip(getItemText(cell, converter));
tooltip.setWrapText(true);
tooltip.prefWidthProperty().bind(cell.widthProperty());
cell.setTooltip(tooltip);
}
}
}
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());
}
}
@TurekBot
Copy link
Author

TurekBot commented Aug 3, 2017

I added a tooltip to the table cell. This way the user can read the text without editing it.

@TurekBot
Copy link
Author

I made an improvement to the text area sizing. See revision on 3/29/2018.

@MP3DSL
Copy link

MP3DSL commented Jun 24, 2020

So I added these lines of code to the cancelEdit() function above super.cancelEdit():

if(!textArea.getText().equals(getItemText(this, getConverter()))) {
Alert a = new Alert(AlertType.CONFIRMATION);
a.setHeaderText("Are you sure you want to cancel the edit?\nAny changes you've made won't be saved");
a.showAndWait();
if(a.getResult() == ButtonType.CANCEL) {
return;
}
}

However what I'm realizing is that cancelEdit() is being called whether the text edit is being confirmed or canceled.
What I'm basically trying to do is have a confirmation that the user wants to cancel the editing of said text, and eventually add one for committing an edit as well which would require an commitEdit(T newValue) overrided function, but even adding it doesn't fix this problem.

It works perfectly fine for the cancel, but I don't understand why it's appearing for an edit confirmation, and on top of that popping up an additional time when the first prompt is cancelled

I also added 1-click copy support if that interested you to have as it was a major time savor in my project

@cetoh
Copy link

cetoh commented Nov 23, 2020

You should add a null check here:


       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).
       if (text != null) {
            textArea.prefHeightProperty().bind(Bindings.createDoubleBinding(() ->
                    text.getBoundsInLocal().getHeight(), text.boundsInLocalProperty()).add(20)
            );
      }

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