2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

【メモ】Unity Editor拡張 GraphView BuildContextualMenu

Posted at

GraphView ノード追加時の右クリックメニューの独自実装について

※メモ代わりのためGraphViewによる説明は省きます

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

準備"GraphViewにノードを追加する"

sample.gif

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

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;
    }
}
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);
    }
}
SampleNode.cs
public class SampleNode : SampleGraphNode {

    public SampleNode () : base () {

    }
}

■BuildContextualMenuをオーバライドしてメニューを実装する

・SampleNodeGraphViewにBuildContextualMenuを実装する
※手こずったこと・・・ Create Nodeメニュー実装でOnContextMenuNodeCreateとRequestNodeCreationが必要
RequestNodeCreation側がinternal classのGUIViewを扱っていて直接流用できない
image.png
代案としてGUIViewの利用はメニューサブウィンドウを開くためのスクリーン座標を取得しているだけなので
SampleEditorWindowのウィンドウ座標を代替えとすれば問題なく機能する。

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 });
    }
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) {
        // ペースト処理 シリアライズからノード生成
    }
}
2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?