File Tree View Sample(Part3) Drag and Drop

2013年のJavaFX Advent Calendar 2013 - Adventarの2日目です。in English here 2013-12-05 - tomoTakaの日記
前日は@さんの曲線のアニメーション - JavaFX in the Boxです。
明日は@さんのJavaFXでめくるエフェクト!! | るーつにゃんブログです。
JavaFXで作成中のFile Tree ViewFile Tree View Sample(Part 2) - tomoTakaの日記に以下の1機能を追加したので、その実装について書きました。

  • Drag and Dropすることでファイルをコピー

この記事Drag-and-Drop Feature in JavaFX Applications | JavaFX 2 Tutorials and Documentationを参考に実装してみました。JavaFXでは、Drag and Dropがとても簡単に実装できます。

  • Javaのバージョン

  • 画面1(コピーするファイルを選択してクリック)

  • 画面2(ドラッグ中、ディレクトリの上にドラッグした時に背景色を変更)

ここでコピー元のファイル名「t2.txt」の表示が消えるのは?です。

  • 画面3(ドラッグ先に同じ名前のファイルが存在する場合、ダイアログを表示して上書き確認)

  • 画面4(ドラッグ先がドラッグ中ファイルのディレクトリの場合、背景色はブルーにしない)


  • code1(ドラッグ元の設定)

・ドラッグ元のセルの「setOnDragDetected」メソッド

  1. ファイルのみを対象にしたいので「isLeaf」で判定
  2. ドラッグ処理として「コピー」を設定
  3. ドラッグ元のコンテントである「File」を「Dragboard」に設定

(ClipboardContentにPathを設定するメソッドがなかったのでファイルに変換)

        cell.setOnDragDetected(event -> {
            TreeItem<PathItem> item = cell.getTreeItem();
            if (item != null && item.isLeaf()) {
                Dragboard db = cell.startDragAndDrop(TransferMode.COPY);
                ClipboardContent content = new ClipboardContent();
                List<File> files = Arrays.asList(cell.getTreeItem().getValue().getPath().toFile());
                content.putFiles(files);
                db.setContent(content);
                event.consume();
            }
        });
  • code2(ドラッグ先の設定)

・ドラッグ先のセルの「setOnDragOver」メソッド

  1. ドラッグ先としてここでは、ディレクトリを対象にしたいので「!isLeaf」で判定
  2. ドラッグ元と同じセルは対象外
  3. ドラッグ先がドラッグ中ファイルのディレクトリも対象外(画面4の場合)
  4. ドラッグ処理として「コピー」を設定

(moveも設定したかったのですが、moveイベントが「null」になるようなので、また後日調べる予定です)

        cell.setOnDragOver(event -> {
            TreeItem<PathItem> item = cell.getTreeItem();
            if ((item != null && !item.isLeaf()) &&
                    event.getGestureSource() != cell &&
                    event.getDragboard().hasFiles()) {
                Path targetPath = cell.getTreeItem().getValue().getPath();
                PathTreeCell sourceCell = (PathTreeCell) event.getGestureSource();
                final Path sourceParentPath = sourceCell.getTreeItem().getValue().getPath().getParent();
                if (sourceParentPath.compareTo(targetPath) != 0) {
                    // ドラッグ先がドラッグ中ファイルのディレクトリでない場合
                    event.acceptTransferModes(TransferMode.COPY);
                }
            }
            event.consume();
        });
  • code3(ドラッグ先の設定)

・ドラッグ先のセルの「setOnDragEntered」メソッド

  1. ドラッグ先は、code2と同じ条件で設定
  2. ドラッグ先にマウスがあわさった時(まだドラッグ中)にセルの背景色をブルーにする
  3. 画面2のようにディレクトリ上にマウスがある時に背景色を変更する処理はここで実装
        cell.setOnDragEntered(event -> {
            TreeItem<PathItem> item = cell.getTreeItem();
            if ((item != null && !item.isLeaf()) &&
                    event.getGestureSource() != cell &&
                    event.getDragboard().hasFiles()) {
                Path targetPath = cell.getTreeItem().getValue().getPath();
                PathTreeCell sourceCell = (PathTreeCell) event.getGestureSource();
                final Path sourceParentPath = sourceCell.getTreeItem().getValue().getPath().getParent();
                if (sourceParentPath.compareTo(targetPath) != 0) {
                    // ドラッグ先がドラッグ中ファイルのディレクトリでない場合
                    cell.setStyle("-fx-background-color: powderblue;");
                }                
            }
            event.consume();
        });
  • code4(ドラッグ先の設定)

・ドラッグ先のセルの「setOnDragExited」メソッド

  1. code3で背景色をブルーにしていたので、マウスがでていった時に元の背景色に戻す。
        cell.setOnDragExited(event -> {
            cell.setStyle("-fx-background-color: white");
            event.consume();
        });
  • code5(ドラッグ先の設定)

・ドラッグ先のセルの「setOnDragDropped」メソッドで、実際のドラッグ処理を実装

  1. コピー先に同じファイルが存在する場合、上書きするかを確認するためのダイアログ画面を表示(ダイアログ画面の実装は「code7」参照)
  2. 「ok」「cancel」どちらのボタンがクリックされたかは、「replaceProp(BooleanProperty)」に設定
  3. ファイルのコピー処理を非同期に実行(ファイルのコピー処理は「code6」参照)
  4. コピーしたファイルをTree Viewに表示(上書き処理した場合はすでに表示されているので不要)
        cell.setOnDragDropped(event -> {
            Dragboard db = event.getDragboard();
            boolean success = false;
            if (db.hasFiles()) {
                final Path source = db.getFiles().get(0).toPath();
                final Path target = Paths.get(
                        cell.getTreeItem().getValue().getPath().toAbsolutePath().toString(),
                        source.getFileName().toString());
                if (Files.exists(target, LinkOption.NOFOLLOW_LINKS)) {                    
                    Platform.runLater(() -> {
                        BooleanProperty replaceProp = new SimpleBooleanProperty();
                        // 上書き確認のダイアログ表示
                        CopyModalDialog dialog = new CopyModalDialog(stage, replaceProp);
                        replaceProp.addListener((ObservableValue<? extends Boolean> ov, Boolean oldValue, Boolean newValue) -> {
                            if (newValue) {
                                // ダイアログ画面で「OK」がクリックされた場合、ファイルのコピー処理を非同期に実施
                                FileCopyTask task = new FileCopyTask(source, target);
                                service.submit(task);
                            }
                        });
                    });
                } else {
                    // コピー先に同じファイルが存在しない場合、ファイルのコピー処理を非同期に実施
                    FileCopyTask task = new FileCopyTask(source, target);
                    service.submit(task);
                    task.setOnSucceeded(value -> {
                        Platform.runLater(() -> {
                            // Tree Viewのコピー先ディレクトリ配下にコピーしたファイルを追加
                            TreeItem<PathItem> item = PathTreeItem.createNode(new PathItem(target));
                            cell.getTreeItem().getChildren().add(item);
                        });
                    });
                }
                success = true;
            }
            event.setDropCompleted(success);
            event.consume();
        });
  • code6(FileCopyTask.java)

・ファイルのコピーを実施する処理をBackgroundで実行するために、「Task」クラスを拡張

public class FileCopyTask extends Task<Void> {
    private Path source;
    private Path target;

    public FileCopyTask(Path source, Path target) {
        this.source = source;
        this.target = target;
    }    
    @Override
    protected Void call() throws Exception {
        Files.copy(this.source, this.target, StandardCopyOption.REPLACE_EXISTING);
        return null;
    }
}
  • code7(CopyModalDialog.java)

・コピー先に同じファイルが存在する場合、上書きするかを確認するためのダイアログ画面を表示
・「ok」「cancel」どちらのボタンがクリックされたかは、「replaceProp(BooleanProperty)」に設定

public class CopyModalDialog {    
    public CopyModalDialog(Stage owner, final BooleanProperty replaceProp) {
        final Stage dialog = new Stage(StageStyle.UTILITY);
        dialog.initOwner(owner);
        dialog.initModality(Modality.APPLICATION_MODAL);
        GridPane root = new GridPane();
        root.setPadding(new Insets(30));
        root.setHgap(5);
        root.setVgap(10);
        Label label = new Label("既に存在します。上書きしますか?");
        Button okButton = new Button("OK");
        okButton.setOnAction(event -> {
            replaceProp.set(true);
            dialog.hide();
        });
        Button cancelButton = new Button("Cancel");
        cancelButton.setOnAction(event -> {
            replaceProp.set(false);
            dialog.hide();
        });
        root.add(label, 0, 0, 2, 1);
        root.addRow(1, okButton, cancelButton);
        dialog.setScene(new Scene(root));
        dialog.show();
    }
}

コードはここtomoTaka01/FileTreeViewSample: JavaFX File ... - GitHubにアップしています。
ドラッグ処理中に上書きダイアログを表示するのに「Platform.runLater」を使用するなど、非同期処理はこの記事Java技術最前線 - JavaFX 2ではじめる、GUI開発 第14回 非同期処理:ITproで勉強しました!
今後もディレクトリのコピーなど、機能を追加していければと思っています。