Drag and Drop on JavaFX Part 3 (Sortable)

今回も前回に引き続き Drag and Drop です。
今回は Drag and Drop を応用した機能の一つでもある Sortable がテーマです。
今回紹介する Sortable は ある Node を Drag on Drop で並び替えれるようにするものです。
もちろん、前回の Draggable と同様に script.aculo.us を参考に実装しましたが、基本機能のみで オプションは全くありません。
オプションは今後に期待ということで...

サンプル

以下のサンプルは VBox の中の Node を Drag on Drop で並び替えられるようにする Sortable の実装例です。
普通に Drag on Drop で並び替えられるだけではつまらないので、今回は iPhone ライクなリストにしてみました。
何が普通と違うのかというと、右端の 3本線のエリアしかドラッグできないのです。(たったそれだけですが...)

以下は、上記サンプルのコーディング例です。
Main.fx

Sortable の使い方は簡単で 最後の一行を追加するだけです。
node 属性に 並び替えできるようにしたい Node の親の Node (Group) を指定します。
今回は、ドラッグエリアを3本線のエリアのみにするため、handle 属性に ドラッグエリアのスタイルクラス名 "handle" を指定しています。
handle 属性の説明は 『 Drag and Drop on JavaFX Part 1 』 の Draggable.fx を参照してください。

def BOOKMARKS = [
    "JavaFX",
    "Yahoo! JAPAN",
    "Google",
    "はてな",
    "Amazon.co.jp",
    "楽天",
    "livedoor",
    "Wikipedia",
    "Project Kenai",
    "GitHub"
];

Stage {
    scene: Scene {
        content: vbox = VBox {
            spacing: .5
            content: for (bookmark in BOOKMARKS ) {
                Stack {
                    nodeHPos: javafx.geometry.HPos.LEFT
                    content: [

                        // テキスト背景
                        Rectangle {
                            width: 220, height: 30, fill: Color.WHITE
                        }

                        // テキスト
                        Text { content: "{bookmark}", translateX: 5 }

                        // ドラッグ領域
                        Rectangle {
                            styleClass: "handle"
                            width: 30, height: 30
                            translateX: 220.5
                            fill: Color.WHITE
                        }
                        // 三本線
                        for (y in [-5..5 step 5]) {
                            Rectangle {
                                width: 16, height: 3
                                translateX: 227, translateY: y
                                fill: Color.LIGHTGRAY
                            }
                        }
                    ]
                }
            }
        }
    }
}

Sortable { node: vbox, handle: "handle" }
Sortable.fx

ここで特筆すべきは、onMousePressed で...

  1. ドラッグした Node の LayoutInfo#managed を false にする。
  2. ドラッグした Node の toFront() メソッドを実行する。
の2つぐらいです。その他はソースをみてもらえばだいたいわかるのではないでしょうか?

まず、1. ですが、これは レイアウトに影響を与えず、レイアウト内の Node を移動するために必要です。
これをしないと、Node をドラッグするとともにレイアウトが崩れてしまうのです。

次に 2. ですが、これは ドラッグした Node が 他の Node に隠れないようにするために必要です。
JavaFX のレイアウトは、content に設定されたシーケンスの順番に Z 方向に配置されます。
content の一番最初が 最下面になります。
従って、toFront() せずに content の一番最初の Node をドラッグすると...
他の Node の下に表示されてしまうので、ドラッグした Node が消えてしまったように見えるのです。
ただし、toFront() には少しクセがあり、実行すると、その Node が再上面に表示されるだけでなく...
なんと!! content に設定されたシーケンスの最後の要素に移動してしまうのです。

なお、今回の Sortable の実装は、VBox しか想定していません。
他のレイアウトで正しく動くかどうかは不明です...
多分、まともに動かないだろうなぁ...
まぁ、VBox で利用することが一番多そうだし、良しとするかな...

public class Sortable {
    public-init var node: Group;
    public-init var handle: String;    

    var dummy = Rectangle { fill: Color.WHITE };
    var draggedNode: Node;
    var draggedIndex = 0;

    postinit {
        for (_node in node.content) {
    
            Draggable {

                handle: handle
                constraint: Draggable.VERTICAL;
                node: _node

                // dummy ノード 準備
                onMousePressed: function(e: MouseEvent) {
                    draggedNode  = e.source;                   
                    draggedIndex = Sequences.indexByIdentity(
                                            node.content, draggedNode);

                    draggedNode.layoutInfo = LayoutInfo { managed: false };
                    draggedNode.toFront();  // 再上面に移動 (これをしないと他の Node に隠れてしまう...)
                                            // また、toFront() すると 必ず node.content の最後の要素に移動してしまう。

                    insert dummy before node.content[draggedIndex];
                    dummy.width  = draggedNode.boundsInLocal.width;
                    dummy.height = draggedNode.boundsInLocal.height;
                }

                // dummy ノード 移動
                // ドラッグした Node が 他の Node と 50% 以上 重なった場合に dummy を移動する。
                onMouseDragged: function(e: MouseEvent) {
                    var min = draggedNode.boundsInParent.minY;
                    var max = draggedNode.boundsInParent.maxY;
                    for (_node in node.content) {
                        var _min = _node.boundsInParent.minY;
                        var _max = _node.boundsInParent.maxY;
                        var overlap =
                            if (_min < min and min < _max) {
                                (_max - min) / (max - min)
                            } else if (_min < max and max < _max) {
                                (max - _min) / (max - min)
                            } else {
                                0.0
                            }

                        if (overlap > 0.5) {
                            var index = indexof _node;
                            if (index != draggedIndex) {    // ドラッグした Node 以外
                                var dummy = node.content[draggedIndex];
                                delete node.content[draggedIndex];
                                insert dummy before node.content[index];
                                draggedIndex = index;
                            }
                            break;
                        }
                    }
                }

                // Drop 処理
                onMouseReleased: function(e: MouseEvent) {
                    delete draggedNode from node.content;
                    node.content[draggedIndex] = draggedNode;

                    draggedNode.translateY = 0.0;
                    draggedNode.layoutInfo = LayoutInfo { managed: true };

                    draggedNode = null;
                    draggedIndex = -1;
                }
            }
        }
    }
}