Unityエディターのそのものを拡張する　第3回ツールバー拡張編

# はじめに
この記事は「Unityエディターそのものを拡張して作業を効率化する」から派生している全3回のうち、第3回目の記事です。
他の記事も気になる場合は[こちら](https://qiita.com/marv_nakazawaka/items/cf9129002420b4a014c9)からご覧ください。

この記事ではUnityエディターのツールバーを拡張して、独自のボタンやラベル、チェックボックス、ドロップダウンといったUIを追加する方法を紹介します。

ここで紹介しているツールはUnity2021.3.9f1を使用しています。
（検証していませんが、おそらくUIElementsがエディターで導入された2019.x以降であれば動作すると思います。）

# 使用例とどんな場面で役に立つか
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3118525/d33d5daa-3e4f-9877-e042-2cc235643059.png)
## 高い頻度で使用するメニューバーやコンテキストメニューの項目をツールバーに表示させる。
![AddMenu.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3118525/74159bd1-a11d-7c15-dc2e-d0b8cd709d61.png)
拡張機能を作成した場合、メニューバーに項目を追加したり、プロジェクトウィンドウを右クリックした際のコンテキストメニューに項目を追加したりすることが多いと思います。これはメソッドにアトリビュートを付けるだけでいいので手軽な方法ではありますが、拡張機能の数が増えてくると探し出して選ぶのも手間になります。また、これらは作った人でなければ周知されない限り、自然に気づくというのはなかなか期待しにくいので、その拡張機能を必要としている人なのに、たまたま見落としてた（またはその日休んでいた）場合や、実装後の途中参加で教え忘れていた場合には埋もれることになってしまいます。
そこでツールバーで目立つように表示させることで、気づきやすく、拡張機能のクイックアクセス的な使い方をすることが可能です。

## Play時にシーン切り替えの手間を省く
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3118525/e3d65df6-ad8a-7871-5a08-3e5201126528.png)
例えば「シーン1」、「シーン2」、「シーン3」という3つのシーンがあるUnityプロジェクトで、シーン1の作業中にシーン3から開始したい場合に、デフォルトのPlayボタンとは別で、独自に指定のシーンから開始するボタンを追加することも可能です。実際のプロジェクトではどのシーンにいても、ゲーム本編のエントリとなるシーンから開始できるようにしたり、3Dモデルのビューワーのシーンを開始したりできるようにしていました。
このようなわざわざEditorWindowを作るほどではないけど、ボタン以外のドロップダウンなどを使いたい、使う頻度の高い機能の実装にも便利です。

これらの機能はほとんどどんなプロジェクトでも役に立つのではないかと思っています。
強いてあげるのであれば、すでにツールバーを拡張するアセットを入れていてバッティングしてしまう、ディスプレイが小さすぎてツールバーの拡張が出来る余白がないという場合以外は導入を検討する価値があると思います。


# 具体的な実装方法
## 1. ツールバーの左右の領域のVisual Elementを取得する。
ツールバーも公開はされていないので、リフレクションで所得してくる必要があります。
ツールバーはEditorWindowを取得していないため、m_Rootというメンバーを取得します。
このm_Rootがツールバーのルートとなるビジュアルエレメントです。
```cs:CustomToolbar.cs
[InitializeOnLoad]
public class CustomToolbar
{
    private static VisualElement rootVisualElement;
    private static VisualElement leftZoneAlign;
    private static VisualElement rightZoneAlign;

    static CustomToolbar()
    {
        EditorApplication.update -= OnEditorUpdate;
        EditorApplication.update += OnEditorUpdate;
    }
        
    private static void OnEditorUpdate()
    {
        if (rootVisualElement != null)
            return;
            
        RebuildToolbar();
    }

    [MenuItem("Tools/Redraw Custom Toolbar")]
    public static void RebuildToolbar()
    {
        // ツールバーの取得
        var toolbarType = typeof(Editor).Assembly.GetType("UnityEditor.Toolbar");
        var toolbars = Resources.FindObjectsOfTypeAll(toolbarType);
        var currentToolbar = toolbars.FirstOrDefault();

        if (currentToolbar == null)
            return;

        // ツールバーのルートのビジュアルエレメントを取得
        var bindingFlags = BindingFlags.NonPublic | BindingFlags.Instance;
        var fieldInfo = toolbarType.GetField("m_Root", bindingFlags);
        var toolbar = Convert.ChangeType(currentToolbar, toolbarType);
        var field = fieldInfo.GetValue(toolbar);
        rootVisualElement = field as VisualElement;

        if (rootVisualElement != null)
        {
            leftZoneAlign = rootVisualElement.Q("ToolbarZoneLeftAlign");
            rightZoneAlign = rootVisualElement.Q("ToolbarZoneRightAlign");
        }
    }
}
```

## 2. 取得したVisual Elementの子として独自のUIを追加していく。
3.1で取得したleftZoneAlignとrightZoneAlignがそれぞれツールバーの左側と右側の領域になります。
これらには元からUnity側で追加したビジュアルエレメントがあるので、それらと兄弟の関係になるビジュアルエレメントを追加して、そこに自分独自のUIを追加することが可能です。
```cs:CustomToolbar.cs
var customLeftToolbar = new VisualElement();
leftZoneAlign.Add(customLeftToolbar);

var myLabel = new Label();
var myButton = new Button();
customLeftToolbar.Add(myLabel);  // 独自のラベルを追加
customLeftToolbar.Add(myButton); // 独自のボタンを追加
```

もし追加したい機能があらかじめ決まっていて、メンテナンスも必要ないという場合にはここまでの内容で十分です。
ですが、開発していく中でツールバーに拡張する要素を増やしたい、他のプロジェクトでも汎用的に利用できるものにしたいという場合には、管理用のクラスを作り、それを通してツールバーに要素の追加や削除を行えるようにするのがおすすめです。

## 3.ツールバーの片側領域を管理するクラスを実装する。
1．でToolbarZoneLeftAlignとToolbarZoneRightAlignというビジュアルエレメントを取得していましたが、この子として設定するための片側のZoneを管理するクラスを作ります。
このクラスはVisual Elementでもいいですが、もしツールバーの領域が足りなくて見切れた場合にも一応使えるようにScrollViewを継承して作るのがいいのではないかと思います。

```cs:SideToolbar.cs
using System;
using UnityEditor.UIElements;
using UnityEngine.UIElements;

public class SideToolbar : ScrollView
{
    private List<VisualElement> toolbarElements = new();

    public SideToolbar()
    {
        // 垂直方向のスクロールバーは隠す
        verticalScrollerVisibility = ScrollerVisibility.Hidden;
            
        // 内容更新
        UpdateContents();
    }

    private void UpdateContents()
    {
        contentContainer.Clear();
        foreach(var toolbarElement in toolbarElements)
        {
            contentContainer.Add(toolbarElement);
        }
    }

    public Label CreateLabel(string labelText)
    {
        var label = new Label(labelText);
        toolbarElements.Add(label);
        UpdateContents();

        return label;
    }

    public Button CreateButton(string buttonText, Action onClicked)
    {
        var button = new ToolbarButton
        {
            text = buttonText,
            clickable = new Clickable(onClicked),
        };
        toolbarElements.Add(button);
        UpdateContents();

        return Button;
    }

    public void DeleteElement(VisualElement element)
    {
        if(!toolbarElements.Contains(element))
            return;

        toolbarElements.Remove(element);
    }
}
```

## 4.ツールバーの片側管理クラスをそれぞれのZoneに追加する。
3で作成した片側管理クラスをツールバーの拡張管理クラスのメンバーとして持たせ、ツールバーの左右のZoneのビジュアルエレメントを取得した後のタイミングでAddすることで子としてぶら下げます。
```cs:CustomToolbar.cs
[InitializeOnLoad]
public class CustomToolbar
{       
    private static SideToolbar leftToolbar;
    private static SideToolbar rightToolbar;

    public static SideToolbar Left
    {
        get
        {
            if (leftToolbar == null)
            {
                leftToolbar = new SideToolbar();
                leftToolbar.SetupAsLeft();
            }

            return leftToolbar;
        }
    }
    public static SideToolbar Right
    {
        get
        {
            if (rightToolbar == null)
            {
                rightToolbar = new SideToolbar();
                rightToolbar.SetupAsRight();
            }

            return rightToolbar;
        }
    }

    [MenuItem("Tools/Redraw Custom Toolbar")]
    public static void RebuildToolbar()
    {
        // ツールバーの取得
        var toolbarType = typeof(Editor).Assembly.GetType("UnityEditor.Toolbar");
        var toolbars = Resources.FindObjectsOfTypeAll(toolbarType);
        var currentToolbar = toolbars.FirstOrDefault();

        if (currentToolbar == null)
            return;

        // ツールバーのルートのビジュアルエレメントを取得
        var bindingFlags = BindingFlags.NonPublic | BindingFlags.Instance;
        var fieldInfo = toolbarType.GetField("m_Root", bindingFlags);
        var toolbar = Convert.ChangeType(currentToolbar, toolbarType);
        var field = fieldInfo.GetValue(toolbar);
        rootVisualElement = field as VisualElement;

        if (rootVisualElement != null)
        {
            leftZoneAlign = rootVisualElement.Q("ToolbarZoneLeftAlign");
            rightZoneAlign = rootVisualElement.Q("ToolbarZoneRightAlign");

            leftZoneAlign.Add(Left);
            rightZoneAlign.Add(Right);
        }
    }
}
```

## 5.ツールバーの拡張管理クラスを通してUIを制御する。
アプリ側でツールバーの拡張管理クラスを通してUIを追加したり、削除したりする処理を書きます。
最初から追加させておきたい場合はこのようにInitailizeOnLoadアトリビュートを付けたstaticクラスのコンストラクタでUIを追加するのがいいです。
```cs:MyCustomToolbar.cs
[InitializeOnLoad]
public static class MyCustomToolbar
{        
    private static Button button;

    static MyCustomToolbar()
    {
        AddLeftToolbar();
        AddRightToolbar();
    }

    private static void AddLeftToolbar()
    {
        // Create〇〇メソッドで生成された要素を保持しておくと、後でDeleteElementで消せる
        // ここではボタンを押すとボタンが消える処理
        button = CustomToolbar.Left.CreateButton("ボタン", () => CustomToolbar.Left.DeleteElement(button));
    }
        
    private static void AddRightToolbar()
    {
        // 追加したらそれっきりでいいなら返り値を保持しておく必要はない。
        CustomToolbar.Right.CreateLabel("ラベル");
    }
}
```

それ以外にもゲームの実行中にあるシーンに来た時だけあるUIをツールバーに追加して、そのシーンを抜けたら消したいという場合はそのシーンのゲームオブジェクトにStartで追加して、OnDestoryで削除するようなコンポーネントを追加するといったような使い方も可能です。