【メモ】Unity Editor拡張 GraphView BuildContextualMenu

## GraphView ノード追加時の右クリックメニューの独自実装について
##### ※メモ代わりのためGraphViewによる説明は省きます
BuildContextualMenuによる独自メニュー機能の実装したかったのでメモ
基本的にはBuildContextualMenuをオーバライドして基底クラスの内容を流用する。
ただし中身はdllに固められていてソースは直接追えないが、
Unity TechnologiesがGitHubでソースコードが公開されているのでこちらで確認できる。

https://github.com/Unity-Technologies/UnityCsReference/blob/master/Modules/GraphViewEditor/Views/GraphView.cs

#### 準備"GraphViewにノードを追加する"
![sample.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/188947/fd2cc922-9361-80a3-ec28-557bd145b5be.gif)

##### ■必要なものは下記ソース
- エディタ上で開くEditorWindowを継承したウィンドウ用のクラス   
→SampleEditorWindow.cs
- GraphViewを継承した右クリックメニューを実装するGraphViewクラス   
→SampleNodeGraphView.cs
- 右クリックメニュー("Create Node")押下後のノード検索と選択作成するクラス   
→SearchNodeWindow.cs
- GraphView.Nodeを継承したベースにあたるノードクラス   
→SampleGraphNode.cs
- ベースにあたるノードクラスを継承したサンプルノードクラス   
→SampleNode.cs


```C#:SampleEditorWindow.cs
public class SampleEditorWindow : EditorWindow {

    public SampleNodeGraphView sampleGraphView;
    private string AssetFileName { get; set; }

    [MenuItem ("Window/Open SampleEditorWindow")]
    public static void Open () {
        SampleEditorWindow graphEditor = CreateInstance<SampleEditorWindow> ();
        GetWindow<SampleEditorWindow> ();

        graphEditor.Initialize ();
    }

    public void Initialize () {
        InitGraphView ();
    }

    private void InitGraphView () {
        sampleGraphView = new SampleNodeGraphView () {
            style = { flexGrow = 1 }
        };

        // sampleGraphViewの要素追加
        rootVisualElement.Add (sampleGraphView);
    }
}
```
```C#:SampleNodeGraphView.cs
public class SampleNodeGraphView : GraphView {

    public SampleNodeGraphView () : base () {

        var searchNodeWindow = ScriptableObject.CreateInstance<SearchNodeWindow> ();
        searchNodeWindow.Initialize (this);

        // 右クリック操作でメニュー("Create Node")を選択後に呼び出されるノード検索と選択作成するAction
        nodeCreationRequest += context => {
            SearchWindow.Open (new SearchWindowContext (context.screenMousePosition), searchNodeWindow);
        };
    }

    // Node間の接続処理
    public override List<Port> GetCompatiblePorts (Port startAnchor, NodeAdapter nodeAdapter) {
        var compatiblePorts = new List<Port> ();
        foreach (var port in ports.ToList ()) {

            // 接続先ノードが自身でない
            // 接続先ポートが同入出力ポートでない
            // 接続先ポートが同種ポートである
            if (startAnchor.node == port.node ||
                startAnchor.direction == port.direction ||
                startAnchor.portType != port.portType) {
                continue;
            }

            compatiblePorts.Add (port);
        }
        return compatiblePorts;
    }

```
```C#:SearchNodeWindow.cs
public class SearchNodeWindow : ScriptableObject, ISearchWindowProvider {

    private SampleNodeGraphView sampleGraphView;

    public void Initialize (SampleNodeGraphView sampleGraphView) {
        this.sampleGraphView = sampleGraphView;
    }

    List<SearchTreeEntry> ISearchWindowProvider.CreateSearchTree (SearchWindowContext context) {
        var entries = new List<SearchTreeEntry> ();
        entries.Add (new SearchTreeGroupEntry (new GUIContent ("Create Node")));

        foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies ()) {
            foreach (var type in assembly.GetTypes ()) {
                if (type.IsClass && !type.IsAbstract && (type.IsSubclassOf (typeof (SampleGraphNode)))) {
                    entries.Add (new SearchTreeEntry (new GUIContent (type.Name)) { level = 1, userData = type });
                }
            }
        }

        return entries;
    }

    bool ISearchWindowProvider.OnSelectEntry (SearchTreeEntry searchTreeEntry, SearchWindowContext context) {

        // Editorウィンドウ取得
        var graphEditorWindow = Resources.FindObjectsOfTypeAll<EditorWindow> ()
            .FirstOrDefault (window => window.GetType ().Name == "GraphEditorWindow");

        var type = searchTreeEntry.userData as System.Type;
        var node = Activator.CreateInstance (type) as SampleGraphNode;
        sampleGraphView.AddElement (node);

        return true;
    }
}
```
```C#:SampleGraphNode.cs
public class SampleGraphNode : Node {

    public Port inputPort;
    public Port outputPort;

    public SampleGraphNode () {
        inputPort = Port.Create<Edge> (Orientation.Horizontal, Direction.Input, Port.Capacity.Single, typeof (Port));
        inputPort.portName = "In";
        inputContainer.Add (inputPort);

        outputPort = Port.Create<Edge> (Orientation.Horizontal, Direction.Output, Port.Capacity.Single, typeof (Port));
            outputPort.portName = "Out";
            outputContainer.Add (outputPort);
    }
}
```
```C#:SampleNode.cs
public class SampleNode : SampleGraphNode {

    public SampleNode () : base () {

    }
}
```

#### ■BuildContextualMenuをオーバライドしてメニューを実装する
・SampleNodeGraphViewにBuildContextualMenuを実装する
※手こずったこと・・・ Create Nodeメニュー実装でOnContextMenuNodeCreateとRequestNodeCreationが必要
RequestNodeCreation側がinternal classのGUIViewを扱っていて直接流用できない
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/188947/6e7ebeae-0751-156e-2647-00c2b788fb6f.png)
代案としてGUIViewの利用はメニューサブウィンドウを開くためのスクリーン座標を取得しているだけなので
SampleEditorWindowのウィンドウ座標を代替えとすれば問題なく機能する。

```C#:
private void RequestNodeCreation (VisualElement target, int index, Vector2 position) {
        if (nodeCreationRequest == null)
            return;

        // Editorウィンドウ取得
        var graphEditorWindow = Resources.FindObjectsOfTypeAll<EditorWindow> ()
            .FirstOrDefault (window => window.GetType ().Name == "SampleEditorWindow");

        Vector2 screenPoint = graphEditorWindow.position.position + position;
        nodeCreationRequest (new NodeCreationContext () { screenMousePosition = screenPoint, target = target, index = index });
    }
```

```C#:SampleNodeGraphView.cs
// ～略
    public override void BuildContextualMenu (ContextualMenuPopulateEvent evt) {
        // Create Nodeメニュー
        if (evt.target is GraphView && nodeCreationRequest != null) {
            evt.menu.AppendAction ("Create Node", OnContextMenuNodeCreate, DropdownMenuAction.AlwaysEnabled);
            evt.menu.AppendSeparator ();
        }
        // Copyメニュー
        if ((evt.target is Node)) {            // ノードが選択されている
            evt.menu.AppendAction (            // menu選択後のAction追加
                "Copy",                        // menu名
                copy => { CopyAction (); },    // 実際に呼び出されれるアクション
                // canCopySelection:コピーが成功/失敗 => (メニュー活性/非活性)
                (Func<DropdownMenuAction, DropdownMenuAction.Status>) (copy => (this.canCopySelection ? DropdownMenuAction.Status.Normal : DropdownMenuAction.Status.Disabled)),
                (object) null);
        }
        // Pasteメニュー
        if (evt.target is UnityEditor.Experimental.GraphView.GraphView) {
            evt.menu.AppendAction (
                "Paste",
                paste => { PasteAction (); },
                (Func<DropdownMenuAction, DropdownMenuAction.Status>) (paste => (this.canPaste ? DropdownMenuAction.Status.Normal : DropdownMenuAction.Status.Disabled)),
                (object) null);
        }
        // Deleteメニュー
        if (evt.target is GraphView || evt.target is Node || evt.target is Group || evt.target is Edge) {
            evt.menu.AppendSeparator ();
            evt.menu.AppendAction ("Delete", (a) => { DeleteSelectionCallback (AskUser.DontAskUser); },
                (a) => { return canDeleteSelection ? DropdownMenuAction.Status.Normal : DropdownMenuAction.Status.Disabled; });
        }
    }
    // Create Nodeメニュー作成
    void OnContextMenuNodeCreate (DropdownMenuAction a) {
        RequestNodeCreation (null, -1, a.eventInfo.mousePosition);
    }

    private void RequestNodeCreation (VisualElement target, int index, Vector2 position) {
        if (nodeCreationRequest == null)
            return;

        // Editorウィンドウ取得
        var graphEditorWindow = Resources.FindObjectsOfTypeAll<EditorWindow> ()
            .FirstOrDefault (window => window.GetType ().Name == "SampleEditorWindow");

        Vector2 screenPoint = graphEditorWindow.position.position + position;
        nodeCreationRequest (new NodeCreationContext () { screenMousePosition = screenPoint, target = target, index = index });
    }

    private void CopyAction () {

        // 1.コピー用シリアライズ登録
        serializeGraphElements = new SerializeGraphElementsDelegate (Copy);

        // 2.コピー用コールバック呼び出し
        CopySelectionCallback ();

    }
    private void PasteAction () {

        // 1.貼り付け用デシリアライズ登録
        unserializeAndPaste = new UnserializeAndPasteDelegate (Paste);
        // 2.ペースト用コールバック呼び出し
        PasteCallback ();
    }

    string Copy (IEnumerable<GraphElement> elements) {
        string data = "";
        // コピー処理 ノードをstringにシリアライズ
        return data;
    }
    void Paste (string operationName, string data) {
        // ペースト処理 シリアライズからノード生成
    }
}
```