import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.geometry.Orientation;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Control;
import javafx.scene.control.Separator;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.scene.text.Text;
import javafx.stage.Stage;
import javafx.util.Callback;

/**
 * Issue with programmatic scroll in TableView with a custom cell factory.
 *
 *
 * This program emulates a chat application when everything typed in the bottom
 * text box is added to the end of a TableView above and scroll to show what is
 * just added by calling scrollTo().
 *
 * The "Text" column of TableView uses a custom cell factory of a Text control
 * to wrap text to accommodate long text value.
 *
 * It works well until a long text is added that cause text to wrap in the table,
 * then the text added will not be fully shown. It gets worse when there are multiple
 * wrapped texts in the view of the TableView. Calling refresh() will not resolve
 * the issue. It will be resolved either by manually scroll up and down, or until
 * all wrapped text scrolled out of the view of the TableView.
 *
 */

public class TableViewScrollTest extends Application {

public static void main(String[] args) {
launch(args);
}

@Override
public void start(Stage primaryStage) throws Exception {

// Create a two column table with a custom cell factory
// to support wrapped text.
_textTable = new TableView<>();
_textTable.setEditable(false);
_textTable.setSelectionModel(null);
        TableColumn<TextRow, String> nameColumn = new TableColumn<>("Name");
        nameColumn.setCellValueFactory(new PropertyValueFactory<>("name"));
        nameColumn.setPrefWidth(100);
        nameColumn.setReorderable(false);
        nameColumn.setResizable(false);
        nameColumn.setSortable(false);
        nameColumn.setStyle("-fx-alignment: center");
        _textTable.getColumns().add(nameColumn);
        TableColumn<TextRow, String> textColumn = new TableColumn<>("Text");
        textColumn.setCellValueFactory(new PropertyValueFactory<>("text"));
        textColumn.setCellFactory(new Callback<TableColumn<TextRow, String>, TableCell<TextRow, String>>() {
            @Override
            public TableCell<TextRow, String> call(TableColumn<TextRow, String> param) {
                TableCell<TextRow, String> cell = new TableCell<>();
                Text text = new Text();
                cell.setGraphic(text);
                cell.setPrefHeight(Control.USE_COMPUTED_SIZE);
                text.wrappingWidthProperty().bind(textColumn.widthProperty().subtract(20));
                text.textProperty().bind(cell.itemProperty());
                return cell;
            }
        });
        textColumn.prefWidthProperty().bind(_textTable.widthProperty().subtract(100));
        textColumn.setReorderable(false);
        textColumn.setResizable(false);
        textColumn.setSortable(false);
         _textTable.getColumns().add(textColumn);
        _textList = FXCollections.observableArrayList();
        _textTable.setItems(_textList);

Separator separator = new Separator();
separator.setOrientation(Orientation.HORIZONTAL);

// Input text field and a button to add the text.
HBox hBox = new HBox(20);
hBox.setAlignment(Pos.CENTER);
_textField = new TextField();
Button button = new Button("Send");
button.setDefaultButton(true);
button.setOnAction(e -> addText());
HBox.setHgrow(_textField, Priority.ALWAYS);
hBox.getChildren().addAll(_textField, button);

VBox vBox = new VBox(15);
vBox.setPadding(new Insets(20, 10, 20, 10));
VBox.setVgrow(_textTable, Priority.ALWAYS);
vBox.getChildren().addAll(_textTable, separator, hBox);

Scene scene = new Scene(vBox);
primaryStage.setScene(scene);
primaryStage.setTitle("TableView Scroll Test :fx.version -"+System.getProperty("javafx.version") );
primaryStage.setMinWidth(400);
primaryStage.setWidth(400);
primaryStage.setMinHeight(400);
primaryStage.setHeight(400);
primaryStage.show();

_textField.requestFocus();
}

private void addText() {
String t = _textField.getText().strip();
if (!t.isEmpty()) {
TextRow row = new TextRow("My Name", t);
_textList.add(row);

_textTable.refresh(); // This will not help.

// The "scroll to last row" below will not work well when
// there are wrapped text line in the view of the TableView.
_textTable.scrollTo(_textList.size() - 1);
}
_textField.setText("");
}

public static class TextRow {

public String getName() {
return _name;
}

public String getText() {
return _text;
}

private TextRow(String name, String text) {
_name = name;
_text = text;
}

private final String _name;
private final String _text;
}

    private TableView<TextRow> _textTable;
private ObservableList<TextRow> _textList;
    private TextField _textField;
} 