JavaFXのTableViewで最後の列を自動制御にする

# 概要
JavaFXのTableViewの列幅の制御で、困ったのでそれの対応方法のメモ。

最後の列を自動制御にして、テーブル幅いっぱいに列が埋まるようにしてみた。

# ColumnResizePolicy
JavaFxのTableViewの列幅の制御については、ColumnResizePolicyという仕組みがある。  
これは、初期描画時や列幅の調整時に、どのように列幅の制御をするかを決めている仕組みになるようだ。

デフォルトで用意されているのは2種類  

| 定数       |       概要                 |
|:-----------------|:--------------------------|
| UNCONSTRAINED_RESIZE_POLICY | 特に制御は無く、ユーザーが動かした通りに列幅を処理する|
| CONSTRAINED_RESIZE_POLICY   | 初期描画時などは均等の列幅、列の伸縮時はそれに合わせて、他の列を均等に処理する　|

これ以外で独自の処理が必要な場合は実装する必要があり、実態としてはCallbackクラスとなる。

```java
public final void setColumnResizePolicy(Callback<ResizeFeatures, Boolean> callback)
```

この中で、各列の幅の変更を行い、成功した場合はtrueを返すことで実現しているようだ。

個人的に欲しかったのは、最後の列の幅の自動制御で、以下を実現したい    
- 初期描画時、ウィンドウのリサイズ時は、最後の列をウィンドウ幅(ペイン)に合わせたい
- ダブルクリックのオートフィット時にも、最後の列をウィンドウ幅(ペイン)に合わせたい

JavaFxのTableView周りを覗いてどうにか出来ないか探したが、デフォルトではないようで、あきらめて実装。 
何とか目的の動作になった。細かい制御や考慮はしてないけど、動作確認としては問題なさそう。

```java
public class ResizePolicy {

    public static <S> void setLastColumnResizePolicy(TableView<S> tableView) {
        // リサイズポリシーをセットする
        tableView.setColumnResizePolicy(prop -> {
            if (prop.getTable().getWidth() < 0) {
                return false;
            }
            resizeTargetColumn(prop);
            resizeLastColumn(prop);
            return true;
        });

        // ダブルクリック時のオートフィット後のリサイズ処理を追加する
        addResizeBorderDoubleClickedEvent(tableView);
    }

    private static void resizeTargetColumn(TableView.ResizeFeatures<?> prop) {
        var targetColumn = prop.getColumn();
        var delta = prop.getDelta();

        if (delta != 0 && targetColumn.isResizable()) {
            var newWidth = targetColumn.getWidth() + delta;

            // 最大幅、最小幅を考慮して、最終的な幅を設定する
            if (newWidth > targetColumn.getMaxWidth()) {
                targetColumn.setPrefWidth(targetColumn.getMaxWidth());
            } else if (newWidth < targetColumn.getMinWidth()) {
                targetColumn.setPrefWidth(targetColumn.getMinWidth());
            } else {
                targetColumn.setPrefWidth(newWidth);
            }
        }
    }

    private static void resizeLastColumn(TableView.ResizeFeatures<?> prop) {
        var table = prop.getTable();
        // -18.0 をしないとスクロールバーが出る。18.0は画面を見て決めただけ
        var tableContentWidth = table.getWidth() - 18.0;

        // 最後のカラム以外の合計値とテーブルの幅から、最後のカラムの幅を算出する
        var columns = table.getVisibleLeafColumns();
        var columnsOfExcludedLastColumn = columns.stream().limit(columns.size() - 1);
        var widthsOfExcludedLastColumn = columnsOfExcludedLastColumn.map(TableColumnBase::getWidth);
        var lastColumnWidth = widthsOfExcludedLastColumn
                .reduce(tableContentWidth, (remainingWidth, columnWidth) -> remainingWidth - columnWidth);

        if (lastColumnWidth > 0) {
            var lastColumn = columns.get(columns.size() - 1);
            lastColumn.setPrefWidth(lastColumnWidth);
        }
    }

    private static <S> void addResizeBorderDoubleClickedEvent(TableView<S> tableView) {
        // 画面描画時にはskinがnullなので、イベントとしてやりたいことを登録しておく
        tableView.skinProperty().addListener((observable, oldValue, newValue) -> {
            var tableViewSkin = (TableViewSkin<?>) newValue;
            var tableViewSkinNodes = tableViewSkin.getChildren().stream();
            var tableHeaderRowNode = tableViewSkinNodes.filter(node -> node instanceof TableHeaderRow).findFirst();

            tableHeaderRowNode.ifPresent((node) -> {
                var tableHeaderRow = (TableHeaderRow) node;
                var tableColumnHeader = (NestedTableColumnHeader) tableHeaderRow.getChildren().get(1);

                tableColumnHeader.addEventFilter(MouseEvent.MOUSE_PRESSED, mouseEvent -> {
                    // カラム幅の変更については、四角形の線が実際のイベント対象になる
                    // かつダブルクリックのオートフィットを対象とする
                    if (isRectangle(mouseEvent) && isDoubleClick(mouseEvent)) {

                        // クリックされた四角形の線の位置と、カラム幅から対象となるカラムを探し出す
                        var rectangle = (Rectangle) mouseEvent.getTarget();
                        var columnPosition = 0.0;
                        for (var column : tableView.getColumns()) {
                            columnPosition += column.getWidth();
                            if (rectangle.getLayoutX() < columnPosition) {
                                // このタイミングでリサイズポリシーを実行すると、
                                // オートフィットによる幅の変更前なので、実際の実行は後回しにする
                                Platform.runLater(() -> {
                                    var columnResizePolicy = tableView.getColumnResizePolicy();
                                    columnResizePolicy.call(new TableView.ResizeFeatures<S>(tableView, column, -0.1));
                                });
                                break;
                            }
                        }
                    }
                });
            });
        });
    }

    private static boolean isRectangle(MouseEvent mouseEvent) {
        return mouseEvent.getTarget() instanceof Rectangle;
    }

    private static boolean isDoubleClick(MouseEvent mouseEvent) {
        return mouseEvent.getClickCount() == 2 && mouseEvent.isPrimaryButtonDown();
    }

}
```

本当は、staticではなく、クラスとして作ろうとしたが、ジェネリクスの問題で警告が出て、SuppressWarningsで消すのもなんだかなと思って、staticなメソッドにしてしまった。。。

ResizeFeaturesはジェネリクスのクラスなのに、setColumnResizePolicyメソッドでは、生で扱ってるので、自分で型宣言をちゃんとやると、型エラーで受け入れてくれない。  
型を付けないと警告が出る感じです。

なんでこうしたんだろうか。。。