GraphView ノード追加時の右クリックメニューの独自実装について
※メモ代わりのためGraphViewによる説明は省きます
BuildContextualMenuによる独自メニュー機能の実装したかったのでメモ
基本的にはBuildContextualMenuをオーバライドして基底クラスの内容を流用する。
ただし中身はdllに固められていてソースは直接追えないが、
Unity TechnologiesがGitHubでソースコードが公開されているのでこちらで確認できる。
準備"GraphViewにノードを追加する"
■必要なものは下記ソース
- エディタ上で開く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を扱っていて直接流用できない
代案として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) {
// ペースト処理 シリアライズからノード生成
}
}