JavaFX の TableView の選択範囲をクリップボードにコピーする。

# この記事について

将来的に書く予定の「JavaFX で DynamoDB Viewer作ってみた」記事の１ステップ。
結構大きな話になると思うので、少しずつ技術ポイント毎に記事を書いて、ある一定程度の要件を満たせた段階で前述まとめ記事書く予定。

第一回記事：[DynamoDBの情報を読み込んでJavaFXで表示してみる](https://qiita.com/silverbox/items/eba824a9f003df67181e)
第二回記事：[JavaFXで動的にテーブル列を設定する](https://qiita.com/silverbox/items/904a01d561014d800696)
第三回記事：[AWS java SDKでDynamoDBテーブル情報を取得してみる](https://qiita.com/silverbox/items/3caefb782f1bb269490f)

※これまでの記事が基本になってます。メソッドなど細かい部分で再説明していない部分があります。不明点などありましたらコメントなど頂けたら対応しようと思います。

## 今回の追加機能

- テーブル選択エリアの内容をクリップボードにコピー可能にした
- 選択形式を行選択モードとセル選択モードの切り替えが出来る様にした
- コンテキストメニュー及びCtrl＋Cでのコピーに対応した
- データ読み込み時に読み込み中ダイアログ表示

## 現在の進捗

画面だと進捗解りにくいけど、内容をメモ帳などにペースト可能になってます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/530376/ea86d024-63e9-6e1b-0b05-af81c37171d0.png)

[この時点でのソース(github)](https://github.com/silverbox/dynamodbtool/tree/4th-tabledata2clipboard)


# 実装内容確認（クリップボードにコピー）

[JavaFX TableView copy to clipboard](https://stackoverflow.com/questions/11347535/javafx-tableview-copy-to-clipboard) を拝見したところによると、メニューの要素のアクションで、テーブルの中身をクリップボードにコピーしている。
TableView自体にコピペが実装されているわけではないという事が解る。以下の処理を実装する必要がある。

1. 何かの操作にアクションを割り当て
1. アクションの中でクリップボードに登録したい内容をセット


# コピー部分実装

今回は、表計算ソフトに貼り付けできる様、Tab＋改行で構成したテキストをクリップボードにコピーする事にする。
※記載ソース中```tableResultList``` がTableView。

## イベント設定

Scene Builderで TableView:onKeyPressed に ```onTableResultListKeyPressed``` を指定

## イベントハンドラ実装

javaのController側で実装

```java:DynamoDbToolController.java

	// Ctrl＋Cキーイベント
	@FXML
	protected void onTableResultListKeyPressed(KeyEvent ev) {
		if (ev.isControlDown() && ev.getEventType() == KeyEvent.KEY_PRESSED && ev.getCode() == KeyCode.C) {
			actTableLineCopyToClipBoard(null);
		}
	}

	// コンテキストメニューからのイベント
	@FXML
	protected void actTableLineCopyToClipBoard(ActionEvent ev) {
		final ClipboardContent content = new ClipboardContent();
		TableViewSelectionModel<ObservableList<String>> selectedModel = tableResultList.getSelectionModel();
		if (selectedModel.getSelectedItems().size() == 0) {
			return;
		}
		String targetStr = null;
		// セル選択モードか行選択モードで動作切り替え
		if (tableResultList.getSelectionModel().isCellSelectionEnabled()) {
			targetStr = getWholeTableSelectedCellString(selectedModel);
		} else {
			targetStr = getWholeTableSelectedRowString(selectedModel);
		}
		if (targetStr != null) {
			content.putString(targetStr);
			Clipboard.getSystemClipboard().setContent(content);
		}
	}
```

## テーブルの中身をクリップボードにセット

```java:DynamoDbToolController.java

	// セル選択モードでのコピペ用文字列作成
	private String getWholeTableSelectedCellString(TableViewSelectionModel<ObservableList<String>> selectedModel) {
		List<Pair<Integer, Integer>> posList = getPositionList(selectedModel);

		StringBuilder selectedRowStrSb = new StringBuilder();
		int oldRow = posList.get(0).getValue();
		for (Pair<Integer, Integer> position : posList) {
			int newRow = position.getValue();
			String cellStr = tableResultList.getItems().get(newRow).get(position.getKey());
			cellStr = cellStr == null ? "" : cellStr;
			if (oldRow != newRow) {
				selectedRowStrSb.append("\n").append(cellStr);
			} else if (selectedRowStrSb.length() == 0) {
				selectedRowStrSb.append(cellStr);
			} else {
				selectedRowStrSb.append("\t").append(cellStr);
			}
			oldRow = position.getValue();
		}
		return selectedRowStrSb.toString();
	}

	@SuppressWarnings("unchecked")
	// セル選択モードでの位置情報処理。Pair形式に変更＋ソート
	// TablePositionがソート対応してないというエラー出たので。
	private List<Pair<Integer, Integer>> getPositionList(
			TableViewSelectionModel<ObservableList<String>> selectedModel) {
		@SuppressWarnings("rawtypes")
		ObservableList<TablePosition> selPosList = selectedModel.getSelectedCells();

		List<Pair<Integer, Integer>> posList = new ArrayList<>();
		for (TablePosition<ObservableList<String>, String> position : selPosList) {
			posList.add(new Pair<Integer, Integer>(position.getColumn(), position.getRow()));
		}
		posList.sort(new Comparator<Pair<Integer, Integer>>() {
			@Override
			public int compare(Pair<Integer, Integer> o1, Pair<Integer, Integer> o2) {
				int rowdiff = o1.getValue() - o2.getValue();
				if (rowdiff != 0) {
					return rowdiff;
				}
				return o1.getKey() - o2.getKey();
			}
		});
		return posList;
	}

	// 行選択モードでのコピペ用文字列作成
	private String getWholeTableSelectedRowString(TableViewSelectionModel<ObservableList<String>> selectedModel) {
		ObservableList<ObservableList<String>> selectedRecords = selectedModel.getSelectedItems();
		if (selectedRecords.size() == 0) {
			return null;
		}

		StringBuilder selectedRowStrSb = new StringBuilder();
		for (ObservableList<String> record : selectedRecords) {
			if (selectedRowStrSb.length() > 0) {
				selectedRowStrSb.append("\n");
			}
			selectedRowStrSb.append(getOneRowString(record));
		}
		return selectedRowStrSb.toString();
	}

	private String getOneRowString(ObservableList<String> record) {
		StringBuilder oneRowStrSb = new StringBuilder();
		for (String item : record) {
			if (oneRowStrSb.length() > 0) {
				oneRowStrSb.append("\t");
			}
			oneRowStrSb.append(item == null ? "" : item);
		}
		return oneRowStrSb.toString();
	}
```

## 追加：TableViewを複数行選択可能にする

上の動作確認の為に設定。自分が使ってるScene Builderだと設定できないのでControllerで直接。

```java:
	tableResultList.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
```

## 追加：TableViewをセル選択モードにする

同上。デフォルトは行選択。

```java:
	tableResultList.getSelectionModel().setCellSelectionEnabled(true);
```


# ContextMenu で、行選択モード/セル選択モード選択

## Scene Builder 対応

1. TableViewの配下にContextMenu追加。
2. ContextMenuの fxId に ```contextMenuTable``` を指定
3. ContextMenuにMenuItemを追加。
4. MenuItemの fxId に ```menuItemTableResultListCellSelectMode``` を指定
5. 同、On Actionに```actToggleCellSelectMode```を指定

## java側対応

isCellSelectMode はクラス変数に指定済み。
切り替え時に、MenuItemの表示文字列も切り替え

```java:DynamoDbToolController.java
	@FXML
	protected void actToggleCellSelectMode(ActionEvent ev) {
		isCellSelectMode = !isCellSelectMode;
		if (isCellSelectMode) {
			menuItemTableResultListCellSelectMode.setText("Switch to row select mode");
		} else {
			menuItemTableResultListCellSelectMode.setText("Switch to cell select mode");
		}
		tableResultList.getSelectionModel().setCellSelectionEnabled(isCellSelectMode);
	}
```

# データ読み込み時に読み込み中ダイアログ表示部分実装

データ読み込み時に、いわゆる砂時計やぐるぐるアイコンにしたい。対応は以下の処理

- テーブルリスト表示
- データ読み込み時

しかし、JavaFXの仕様か何かで、処理スレッドが終わった時にしか再描画されないらしく、カーソルを変更する事で色々検討してみたが、駄目だった。カーソル変更での対応は諦め、Alertを使用してそれを表現する事にした。

## 読み込み用ダイアログ初期化

シンプルにするため、AlertType.NONE を使用。ただそのままだと、ダイアログが閉じれなくなる。その為、OKボタンを追加。そして、それを不可視にしている。

```java:初期化関数作成
	private void initLoadDialog() {
		dialog = new Alert(AlertType.NONE);
		dialog.setHeaderText(null);
		dialog.setContentText("Now Loading...");
		dialog.getDialogPane().getButtonTypes().add(ButtonType.OK);
		Pane pane = dialog.getDialogPane();
		ObservableList<Node> nodes = pane.getChildren();
		for (Node node : nodes) {
			if (node instanceof ButtonBar) {
				node.setVisible(false);
			}
		}
	}
```

## 読み込み時処理

後にもっと良い読み込み中表現が見つかった時の為に、ダイアログ表示／非表示は関数化。代表してテーブルリスト表示のロジックを紹介。

```java
	@FXML
	protected void actTableListLoad(ActionEvent ev) throws URISyntaxException {
		startWaiting();
		try {
			TableListDao dao = new TableListDao(getConnectInfo());
			TableNameCondType conditionType = TableNameCondType.getByName(cmbTableNameCond.getValue());
			lvTableList.getItems().clear();
			lvTableList.getItems().addAll(dao.getTableList(txtFldTableNameCond.getText(), conditionType));
		} finally {
			finishWaiting();
		}
	}

	private void startWaiting() {
		dialog.show();
	}

	private void finishWaiting() {
		dialog.close();
	}
```

# 次回予定

テーブル中身表示部を動的タブで追加して、複数テーブルの中身を効率的に見れるようにしたい。

# 参考にさせて頂いたページ

## 公式ページ

[Clipboard クラス](https://docs.oracle.com/javase/jp/8/javafx/api/javafx/scene/input/Clipboard.html)
[JavaFX CSSリファレンス・ガイド](https://docs.oracle.com/javafx/2/api/javafx/scene/doc-files/cssref.html#cell)
[Groundbreakers Developer Community](https://community.oracle.com/tech/developers/discussion/2411879/tableview-css-cell-border)

## 皆さんの良記事

[JavaFX の Clipboard API を使う](https://qiita.com/toastkidjp/items/0dc759919d9f1b478b20)
[JavaFX / TableViewを編集可能にする](https://qiita.com/monolith52/items/7c0e82fa527cd5fae9c2)
[JavaFX TableView copy to clipboard](https://stackoverflow.com/questions/11347535/javafx-tableview-copy-to-clipboard)
[JavaFX TreeTableViewの選択色を変更する](https://living-sun.com/ja/java/465870-change-selection-color-of-a-javafx-treetableview-java-css-javafx-treetableview-javafx-css.html)
[JavaFX 8 の TableView で行を選択不可にするための設定方法](https://teratail.com/questions/35062)
[java - アラートタイプなしjavafxを閉じる](https://ja.ojit.com/so/java/1414135)