StatusBarを拡張する

# はじめに

まず初めに`StatusBar`とは何かについてですが、これは`UE5`から追加された機能でエディタ下部に`ContentDrawer`や`OutputLog`を含む様々な機能を呼び出せる、Windowsで言うところのタスクバーのようなものです。

![Untitled.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325493/80fc758a-1ba2-6ae3-f772-1124c96f6c35.png)

今回は[`LanguageSwitcher`](https://www.unrealengine.com/marketplace/en-US/product/5e10055a95f34b609c7afe072a5379ab)というプラグインで上の画像の赤枠のように、この`StatusBar`を拡張してボタンなどを追加したのでその際に分かった注意点などを紹介します。

# やり方

`StatusBar`は基本的にはその他のメニューと変わらず`UToolMenus`を使用して拡張を行います。

``` LanguageSwitcher.Build.cs
	public LanguageSwitcher(ReadOnlyTargetRules Target) : base(Target)
	{
		PCHUsage = PCHUsageMode.NoPCHs;
#if UE_5_2_OR_LATER
		IncludeOrderVersion = EngineIncludeOrderVersion.Latest;
#endif
		
		PublicDependencyModuleNames.AddRange(
			new string[]
			{
				"Core",
			}
		);
		
		PrivateDependencyModuleNames.AddRange(
			new string[]
			{
				"CoreUObject",
				"InputCore",
				"Engine",
				"UnrealEd",
				"Projects",
				"EditorStyle",
				"Slate",
				"SlateCore",
				"ToolMenus", // <-- これ
				"MainFrame",
				"Localization",
				"ApplicationCore",
				"InternationalizationSettings",
			}
		);
	}
```

まずは、`UToolMenus`を使うために`ToolMenus`モジュールを依存するモジュールに追加します。
`UToolMenus`の詳しい使い方などはこちらの記事で分かりやすく解説されています。

https://note.com/posi_ta/n/ne6903e084e78

``` StatusBarExtender.cpp
	void FStatusBarExtender::ExtendStatusBar(const FName& StatusBarName)
	{
		auto* ToolMenus = UToolMenus::Get();
		if (!IsValid(ToolMenus))
		{
			return;
		}
		
		UToolMenu* ToolMenu = ToolMenus->ExtendMenu(StatusBarName);
		if (!IsValid(ToolMenu))
		{
			return;
		}
		
		FToolMenuSection& ToolMenuSection = ToolMenu->AddSection(LanguageSwitcherSectionName);
		ToolMenuSection.AddEntry(
			FToolMenuEntry::InitToolBarButton(
				FLanguageSwitcherCommands::Get().InvokeAssignedShortcut,
				FText::GetEmpty()
			)
		)
		.SetCommandList(FLanguageSwitcherCommands::Get().CommandBindings);
		
		ToolMenuSection.AddEntry(
			FToolMenuEntry::InitComboButton(
				TEXT("ComboButton"),
				FToolUIActionChoice(),
				FNewToolMenuChoice(
					FNewToolMenuDelegate::CreateStatic(&FStatusBarExtender::MakeComboButtonSubMenu)
				),
				FText::GetEmpty(),
				LOCTEXT("ComboButtonTooltip", "You can change the language, locale, and flags to use localized stuff."),
				FSlateIcon(),
				true
			)
		)
		.SetCommandList(FLanguageSwitcherCommands::Get().CommandBindings);
	}
```

上記のコードは関数の引数で拡張する`StatusBar`のメニューの名前を受け取ります。

``` StatusBarExtender.cpp
	ExtendStatusBar(TEXT("LevelEditor.StatusBar.ToolBar"));
```

はじめにで添付した画像のようにレベルエディタの`StatusBar`を拡張するには`LevelEditor.StatusBar.ToolBar`を指定します。

ここまでは特に工夫などせずに従来通りのエディタの拡張手順で拡張することができます。
ただ、これだけではレベルエディタ以外のアセットエディタの`StatusBar`にはボタンなどが追加されません。
`StatusBar`はレベルエディタを含むアセットエディタそれぞれでインスタンスを保持していて、それぞれの`StatusBar`に対して同様の拡張を施す必要があります。

### アセットエディタの`StatusBar`の拡張

`StatusBar`の拡張ポイントの名前は`IToolkitHost::GetStatusBarName`で取得できる名前の末尾に`.ToolBar`を付けた文字列になります。

``` SLevelEditor.cpp
FName SLevelEditor::GetStatusBarName() const
{
	static const FName LevelEditorStatusBarName = "LevelEditor.StatusBar";
	return LevelEditorStatusBarName;
}
```

レベルエディタでは上記のようにオーバーライドされているため、先ほどの拡張では`LevelEditor.StatusBar.ToolBar`を指定しました。

``` SStandaloneAssetEditorToolkitHost.h
FName GetStatusBarName() const override { return StatusBarName; } 
```

レベルエディタ以外のアセットエディタでは上記のようにオーバーライドされています。

``` SStandaloneAssetEditorToolkitHost.cpp
void SStandaloneAssetEditorToolkitHost::Construct( const SStandaloneAssetEditorToolkitHost::FArguments& InArgs, const TSharedPtr<FTabManager>& InTabManager, const FName InitAppName )
{
	// ...

	// Asset editors have non-unique names. For example every material editor is just "MaterialEditor" so the base app name plus a unique number to generate uniqueness. This number is not used across sessions and should never be saved.
	StatusBarName = FName(AppName, ++StatusBarIdGenerator);

	// ...
}
```

`StatusBarName`は`SStandaloneAssetEditorToolkitHost::Construct`で設定されており、コメントにもある通りアセットエディタごとの一意の名前の末尾に連番を付けた形になります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325493/c203e7ff-936e-4cd8-2c40-ec94661f39e1.png)

例えば、ブループリントエディタの場合は上の画像のように`BlueprintEditorApp_[連番]`のような形になります。
これらを踏まえてアセットエディタを開く際に`ToolkitHost`を介して`StatusBarName`を取得し、連番部分を切り取って末尾に`.ToolBar`を付けた名前を指定して`FStatusBarExtender::ExtendStatusBar`を呼び出す必要があります。

``` StatusBarExtender.cpp
	void FStatusBarExtender::Register()
	{
		ExtendStatusBar(TEXT("LevelEditor.StatusBar.ToolBar"));
		
		IMainFrameModule::Get().OnMainFrameCreationFinished().AddStatic(&FStatusBarExtender::HandleOnMainFrameCreationFinished);
	}

	void FStatusBarExtender::HandleOnMainFrameCreationFinished(TSharedPtr<SWindow> InRootWindow, bool bIsNewProjectWindow)
	{
		if (auto* AssetEditorSubsystem = GEditor->GetEditorSubsystem<UAssetEditorSubsystem>())
		{
			AssetEditorSubsystem->OnEditorOpeningPreWidgets().AddStatic(&FStatusBarExtender::HandleOnEditorOpeningPreWidgets);
		}
	}

 	void FStatusBarExtender::HandleOnEditorOpeningPreWidgets(const TArray<UObject*>& AssetsToEdit, IAssetEditorInstance* AssetEditorInstance)
	{
		if (AssetEditorInstance == nullptr)
		{
			return;
		}
		
		FName BuiltStatusBarName;
		{
			const auto* AssetEditorToolkit = static_cast<FAssetEditorToolkit*>(AssetEditorInstance);
			const TSharedRef<IToolkitHost>& ToolkitHost = AssetEditorToolkit->GetToolkitHost();
			
			const FString StatusBarName = ToolkitHost->GetStatusBarName().ToString();
			FString AssetEditorName;
			if (!StatusBarName.Split(TEXT("_"), &AssetEditorName, nullptr))
			{
				return;
			}

			BuiltStatusBarName = FName(AssetEditorName + TEXT(".ToolBar"));
		}
		ExtendStatusBar(BuiltStatusBarName);
	}
```

アセットエディタの`StatusBar`を拡張するタイミングとしては、`StatusBar`の構築時に拡張ポイントのデータが無いといけないため、`UAssetEditorSubsystem::OnEditorOpeningPreWidgets`を使用します。
このデリゲートはアセットエディタが開かれる際に、アセットエディタを構成するウィジェットが構築される前に呼び出されます。

レベルエディタの拡張を含めて、プラグインのモジュールの`StartupModule`で呼び出される`FStatusBarExtender::Register`でデリゲートへのバインドを行いたいですが、モジュールの開始時にはまだサブシステムのインスタンスが作成されていないため、`IMainFrameModule::OnMainFrameCreationFinished`を使用してエディタのメインフレームが生成された後に処理を行います。
`IMainFrameModule::OnMainFrameCreationFinished`は今回のようにタイミングが早すぎて問題がある際によく使用するため覚えておくといいかもしれません。

`UAssetEditorSubsystem::OnEditorOpeningPreWidgets`のバインドする関数の引数では、これから開くアセットエディタのインスタンスを`IAssetEditorInstance`の型で受け取ることができますが、`IAssetEditorInstance`の状態では対応する`ToolkitHost`を取得できないため`FAssetEditorToolkit`にキャストします。
エンジンコードを見る限りでは`FAssetEditorToolkit`を使用せず`IAssetEditorInstance`を継承して作成したアセットエディタは作れないはずなので、キャストが失敗することはありませんが、できれば`IAssetEditorInstance`から`ToolkitHost`を取得できるような形であって欲しいですね。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325493/85fc02ab-54bd-0785-023b-4def33b96523.png)

これで上の画像の赤枠のように、アセットエディタの`StatusBar`も拡張することができました。

# エンジンビルドできるなら

ここまで色々と工夫をして`StatusBar`の拡張を行いましたが、エンジン改造が可能な場合はもっと簡単に拡張することができます。

``` SStatusBar.cpp
TSharedRef<SWidget> SStatusBar::MakeStatusBarToolBarWidget()
{
	RegisterStatusBarMenu();
	
	FToolMenuContext MenuContext;
	RegisterSourceControlStatus();

	return UToolMenus::Get()->GenerateWidget(GetToolbarName(), MenuContext);
}
```

`RegisterSourceControlStatus`の前後に自前の拡張を行う関数なり処理なりを追加することで全てのアセットエディタで`StatusBar`を拡張することができます。

# おまけ

`UToolMenus`の拡張で使用する拡張ポイントの名前は`Editor Preferences` > `Miscellaneous` > `Display UI Extension Points`を`true`にすることでエディタ上に拡張ポイントの名前が表示されます。
(今回紹介した`StatusBar`を拡張する際に使用する拡張ポイントの名前はエンジンコード上から探す必要がありました...)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325493/bc90655b-cf7a-c6f7-c0db-06ccb4f953ca.png)

小技ですが、`Display UI Extension Points`の値を変更した際に適用のためエディタの再起動を求められますが、再起動せずに何かしらのアセットエディタを起動することでエディタを再起動することなく変更を適用することができます。

# おわりに

![Usage.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325493/9a48444d-dbe6-c98e-99fc-368c721ff000.png)

今回紹介した拡張を行った[`LanguageSwitcher`](https://www.unrealengine.com/marketplace/en-US/product/5e10055a95f34b609c7afe072a5379ab)はショートカットキーや上の画像のように`StatusBar`から言語やリージョンを切り替えることができるプラグインです。
同様にエディタ上で動作する機能で今までツールメニューに機能を呼び出す項目を追加していた物の一部は`StatusBar`から呼び出せるようにした方が便利な場合があると思うので、そういった機能を作る際にこの記事が参考になると幸いです。