Drag and Drop on JavaFX Part 5 (FileDroppable)

今回も まだまだ Drag and Drop でいってみる。
今回は 少し script.aculo.us から離れて、ファイルの Drag and Drop を実装してみました。
JavaScript では実現できないし、これぞ RIA の醍醐味ってところでしょうか...

サンプル

画像ファイルをドラッグして9つの枠の1つにドロップするとその画像のサムネイルが表示されます。
サムネイルといっても小さく表示しているだけですが...
もちろん、前回と同様 9つの枠の位置を Drag and Drop で移動することもできます。
これらの処理が、たった10行追加するだけで実現できてしまうのだから、やはり JavaFX ってすごい!!

ファイルの Drag and DropJavaScript ではできないので、
使い方によってはかなりおもしろいものができるのではないでしょうか?
(HTML5 では ファイルの Drag and Drop ができるようになるらしいが...)

安易ですが...
イメージ共有サイトへのファイルアップローダ とか メディアプレイヤー 等に利用するのも面白そう...
時間ができたら、なにか作ってみようかな... (たぶん、無理だろうけど...)

ところで、Mac OS X の場合、なぜか1回目の Drop ができません。
2回目からは正常にできるようなので、1回 Drop した後、あきらめずにもう一度 Drag and Drop してみましょう。


※ ローカルファイルにアクセスするので、セキュリティの警告がでます。

Main.fx

通常のシーングラフのコードに加えて、 最後に Sortable と FileDroppable を生成するだけです。
構造と処理が完全に分離されていて、非常にわかりやすいのではないでしょうか?
もし、Part 1 のように構造と処理が分離されていなかったら... (詳細は Part 2 参照)
複数の処理を Node に追加するために、シーングラフは本来の構造とは全く関係なく複雑になるでしょう。
今回、Part 2 で紹介した内容の確かさが改めて確認できたと思います。

var tile: Tile;
Stage {
    title: "File Drag and Drop"
    resizable: false
    scene: Scene {
        width:  455
        height: 455
        content: tile = Tile {
            columns: 3
            rows: 3
            content: for (i in [1..9]) {
                Stack {
                    id: "{i}"
                    content: [
                        Rectangle {
                            width:  150
                            height: 150
                            fill: Color.BLACK
                            stroke: Color.BLACK
                            strokeWidth: 0.4
                        }
                        Text {
                            content: "Drop image file."
                            fill: Color.GRAY
                        }
                        ImageView {
                            preserveRatio: true
                            fitWidth:  149
                            fitHeight: 149
                            id: "image_{i}"
                        }
                    ]
                }
            }
        }
    }
}

// 並び替え処理 追加
Sortable { node: tile }

// File Drag and Drop 追加
for (node in tile.content) {
    var drop: FileDroppable = FileDroppable {
        node: node
        onDone: function() {
            var view = (node.lookup("image_{node.id}") as ImageView);
            view.image = Image { url: "file:{drop.files[0].getAbsolutePath()}" }
        }
    }
}
FileDroppable.fx

いくつか特殊なことをしていることもあって...
説明を書くのが結構大変そうなので、今回はソースのみ...
説明は次回ということで 乞うご期待!!

実は、以下のコード、Scene を複数使うような場合 正しく機能しません。
通常は Scene を複数使うようなことはないので、実装を簡略化してあります。
その話も次回少し触れたいと思います。

var listener: FileDroppableListener;

public class FileDroppable {

    public-init var node: Node;
    public-read var files: File;
    public var onDone: function();

    init {
        if (listener == null) {
            listener = FileDroppableListener {}
            new DropTarget(
                getPanel(node) as javax.swing.JComponent,
                DnDConstants.ACTION_COPY,
                listener,
                true);
        }
        listener.addFileDroppable(this);
    }

    function getPanel(node: Node) {
        var context = FXLocal.getContext();

        // SGNode
        var _nodeClass = context.findClass("javafx.scene.Node");
        var _getPGNode = _nodeClass.getFunction("impl_getPGNode");

        // Panel
        var _sgNodeClass = context.findClass("com.sun.scenario.scenegraph.SGNode");
        var _getPanel = _sgNodeClass.getFunction("getPanel");
        var _sgNode = _getPGNode.invoke(context.mirrorOf(node)) as FXLocal.ObjectValue;
        return (_getPanel.invoke(_sgNode) as FXLocal.ObjectValue).asObject();
    }

}

class FileDroppableListener extends DropTargetAdapter {

    var droppables: FileDroppable;

    public function addFileDroppable(droppable: FileDroppable) {
        insert droppable into droppables;
    }

    override function dragOver(e: DropTargetDragEvent) {
        for (droppable in droppables) {
            if (contains(droppable.node, e.getLocation())
                    and e.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) {
                e.acceptDrag(DnDConstants.ACTION_COPY);
                return;
           }
        }
        e.rejectDrag();
    }

    override function drop(e: DropTargetDropEvent) {
        for (droppable in droppables) {
            if (contains(droppable.node, e.getLocation())
                    and e.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) {
                e.acceptDrop(DnDConstants.ACTION_COPY);
                var data = e.getTransferable().getTransferData(DataFlavor.javaFileListFlavor);
                droppable.files = for (file in data as List) {
                    file as File;
                }
                e.dropComplete(true);
                droppable.onDone();
                return;
            }
        }
        e.rejectDrop();
    }

    function contains(node: Node, point: Point) {
        return node.boundsInLocal.contains(node.sceneToLocal(point.getX(), point.getY()));
    }
}