UnrealC++でレベルエディタに独自メニューを追加する

#はじめに
![1.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325493/652c659e-06bb-3c1d-b02f-f50add4293e6.png)
以前からプロジェクト用のエディタ拡張やプラグインなどで、
[[UE4] プラグインによるエディタ拡張(2)　エディタのメニューに項目を追加する](https://historia.co.jp/archives/370/)
こちらのヒストリアさんの記事のようにWindowメニュー内に独自のメニューを追加していました。
Windowメニューの中でも便利ではありますが、やはりいつまでもWindowメニューに居候するわけにもいかないということでやり方を調べて分かったのでそれについてご紹介したいと思います。

#つくりかた
エディタ拡張をするのでまずはモジュールタイプを変更します。サンプルプロジェクトはプラグインの形式で作成したので、プロジェクトのエディタモジュールで拡張を行う場合はuprojectを編集してください。
[SamplePlugin.uplugin]
(https://github.com/Naotsun19B/ExtendMenuSample/blob/master/Plugins/SamplePlugin/SamplePlugin.uplugin)

``` SamplePlugin.uplugin
{
	"FileVersion": 3,
	"Version": 1,
	"VersionName": "1.0",
	"FriendlyName": "SamplePlugin",
	"Description": "",
	"Category": "Other",
	"CreatedBy": "",
	"CreatedByURL": "",
	"DocsURL": "",
	"MarketplaceURL": "",
	"SupportURL": "",
	"CanContainContent": true,
	"IsBetaVersion": false,
	"IsExperimentalVersion": false,
	"Installed": false,
	"Modules": [
		{
			"Name": "SamplePlugin",
			"Type": "EditorNoCommandlet",    // ここを「Editor」か「EditorNoCommandlet」に
			"LoadingPhase": "Default"
		}
	]
}
```
余談ですが、モジュールタイプなどについて知りたい場合はこちらの記事がおすすめです。
[ちょっとだけくわしくUE4 モジュールの話【解説編】](https://qiita.com/strv13570/items/7d43502b72aed478c669)

次に今回はレベルエディタのメニューに拡張を施すためBuild.csでLevelEditorモジュールの使用を宣言しておきます。
[SamplePlugin.Build.cs](https://github.com/Naotsun19B/ExtendMenuSample/blob/master/Plugins/SamplePlugin/Source/SamplePlugin/SamplePlugin.Build.cs)

```
public SamplePlugin(ReadOnlyTargetRules Target) : base(Target)
	{
		PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs;

		PublicDependencyModuleNames.AddRange(
			new string[]
			{
				"Core",
			}
			);
			
		
		PrivateDependencyModuleNames.AddRange(
			new string[]
			{
				"CoreUObject",
				"Engine",
				"Slate",
				"SlateCore",
				"LevelEditor",	// 追加.
			}
			);
	}
```

次はヘッダーに拡張ポイントのポインタと拡張するためのコールバック関数を定義していきます。
[SamplePlugin.h](https://github.com/Naotsun19B/ExtendMenuSample/blob/master/Plugins/SamplePlugin/Source/SamplePlugin/Public/SamplePlugin.h)

```SamplePlugin.h
class FSamplePluginModule : public IModuleInterface
{
private:
	// メニューの拡張ポイント.
	TSharedPtr<FExtender> Extender;

public:
	// IModuleInterface interface.
	virtual void StartupModule() override;
	virtual void ShutdownModule() override;
	// End of IModuleInterface interface.

private:
	// メニューバーに拡張内容を登録.
	void OnWindowMenuBarExtension(FMenuBarBuilder& MenuBarBuilder);

	// プルダウンメニューの登録.
	void OnPulldownMenuExtension(FMenuBuilder& MenuBuilder);

	// プルダウンメニュー以下のサブメニューを登録.
	void HandleRegisterSubMenu1(FMenuBuilder& MenuBuilder);
	void HandleRegisterSubMenu2(FMenuBuilder& MenuBuilder);
	void HandleRegisterSubMenu3(FMenuBuilder& MenuBuilder);
};
```

今回はとりあえず3つのサブメニューを独自のプルダウンメニューに追加してみます。
そして次はソールファイル側でセットアップなどを行います。
[SamplePlugin.cpp](https://github.com/Naotsun19B/ExtendMenuSample/blob/6b14107da63346b85c953d3af915541c8af20d95/Plugins/SamplePlugin/Source/SamplePlugin/Private/SamplePlugin.cpp#L10)

```SamplePlugin.cpp
void FSamplePluginModule::StartupModule()
{
	if (IsRunningCommandlet())
	{
		return;
	}

	FLevelEditorModule& LevelEditorModule = FModuleManager::LoadModuleChecked<FLevelEditorModule>(LEVEL_EDITOR_NAME);
	Extender = MakeShared<FExtender>();
	if (Extender.IsValid())
	{
		Extender->AddMenuBarExtension(
			"Help",
			EExtensionHook::After,
			nullptr,
			FMenuBarExtensionDelegate::CreateRaw(this, &FSamplePluginModule::OnWindowMenuBarExtension)
		);
	}

	TSharedPtr<FExtensibilityManager> MenuExtensibilityManager = LevelEditorModule.GetMenuExtensibilityManager();
	if (MenuExtensibilityManager.IsValid())
	{
		MenuExtensibilityManager->AddExtender(Extender);
	}
}
```
まずは`StartupModule`関数内で独自メニューの追加を行います。
`AddMenuBarExtension`関数の引数ですが、まず第一引数は拡張するポイントを文字列で示します。
標準のエディタではレベルエディタのメニューには「File」「Edit」「Window」「Help」があるのでこのいずれかを選びます。
他のプラグインなどで追加されたメニュー名を指定することはできると思いますがモジュールの読み込み順で見つからなかったり、そのプラグインが外された時などに動かなくなってしまったりする可能性があるため標準の物を指定するのが良いと思います。
第二引数は第一引数で指定したメニューの前後どちらに追加するかを`EExtensionHook`型で選択します。

```Framework\MultiBox\MultiBoxExtender.h
/** Where in relation to an extension hook should you apply your extension */
namespace EExtensionHook
{
	enum Position
	{
		/** Inserts the extension before the element or section. */
		Before,
		/** Inserts the extension after the element or section. */
		After,
		/** Sections only. Inserts the extension at the beginning of the section. */
		First,
	};
}
```
`EExtensionHook::First`にすると第一引数でどれを選んでいてもFileの左側、つまりそのメニューの最初に追加されるかと思いましたがこれにするとそもそもメニューが登録されませんでした。

第三引数は`TSharedPtr<FUICommandList>`型でショートカットキーを割り当てることができます。このあたりは話すと長くなるので今回はNULLにしておくことにしましょう。

そして第四引数は独自メニューが選択された時に呼ばれるコールバック関数を指定します。
今回はヘッダーで定義した`OnWindowMenuBarExtension`関数を指定します。

拡張ポイントに独自メニューを追加したら、`MenuExtensibilityManager`に拡張ポイントを追加することで独自のメニューが追加されます。

そしてもちろん登録をしたなら、解除をしなくてはならないのでモジュールの終了時に拡張ポイントの登録を解除します。
[SamplePlugin.cpp](https://github.com/Naotsun19B/ExtendMenuSample/blob/6b14107da63346b85c953d3af915541c8af20d95/Plugins/SamplePlugin/Source/SamplePlugin/Private/SamplePlugin.cpp#L36)

```SamplePlugin.cpp
void FSamplePluginModule::ShutdownModule()
{
	if (Extender.IsValid() && FModuleManager::Get().IsModuleLoaded(LEVEL_EDITOR_NAME))
	{
		FLevelEditorModule& LevelEditorModule = FModuleManager::LoadModuleChecked<FLevelEditorModule>(LEVEL_EDITOR_NAME);
		TSharedPtr<FExtensibilityManager> MenuExtensibilityManager = LevelEditorModule.GetMenuExtensibilityManager();
		if (MenuExtensibilityManager.IsValid())
		{
			MenuExtensibilityManager->RemoveExtender(Extender);
		}
	}
}
```

次にメニューの追加時にプルダウンメニューを登録します。
[SamplePlugin.cpp]
(https://github.com/Naotsun19B/ExtendMenuSample/blob/6b14107da63346b85c953d3af915541c8af20d95/Plugins/SamplePlugin/Source/SamplePlugin/Private/SamplePlugin.cpp#L49)

```SamplePlugin.cpp
void FSamplePluginModule::OnWindowMenuBarExtension(FMenuBarBuilder& MenuBarBuilder)
{
	MenuBarBuilder.AddPullDownMenu(
		LOCTEXT("MenuBarTitle", "Sample Menu"),
		LOCTEXT("MenuBarToolkit", "A description of this menu is given here."),
		FNewMenuDelegate::CreateRaw(this, &FSamplePluginModule::OnPulldownMenuExtension)
	);
}
```
ここは特に難しいことはなく、`AddPullDownMenu`関数で第一引数に表示名、第二引数にオンカーソルで表示される説明文、第三引数にプルダウンメニューが選択されたときに呼ばれるコールバック関数を指定します。
ここで注意しなければならないのは`LOCTEXT`マクロの第一引数の文字列が重複してはいけないということです。
これは独自メニューを拡張するからではなく`LOCTEXT`を使う上で気を付けなければならないことです。
これに関しても話すと長くなるかもしれないので被らないように気を付けておきましょう。

次はプルダウンメニューにサブメニューを追加していきます。
[SamplePlugin.cpp]
(https://github.com/Naotsun19B/ExtendMenuSample/blob/6b14107da63346b85c953d3af915541c8af20d95/Plugins/SamplePlugin/Source/SamplePlugin/Private/SamplePlugin.cpp#L58)

```SamplePlugin.cpp
void FSamplePluginModule::OnPulldownMenuExtension(FMenuBuilder& MenuBuilder)
{
	MenuBuilder.BeginSection("SectionHook", LOCTEXT("SectionNameA", "Section A"));

	MenuBuilder.AddSubMenu(
		LOCTEXT("PulldownMenuTitle1", "Sub Menu 1"),
		LOCTEXT("PulldownMenuToolTip1", "A description of this submenu is given here."),
		FNewMenuDelegate::CreateRaw(this, &FSamplePluginModule::HandleRegisterSubMenu1)
	);

	MenuBuilder.AddSubMenu(
		LOCTEXT("PulldownMenuTitle2", "Sub Menu 2"),
		LOCTEXT("PulldownMenuToolTip2", "A description of this submenu is given here."),
		FNewMenuDelegate::CreateRaw(this, &FSamplePluginModule::HandleRegisterSubMenu2)
	);

	MenuBuilder.EndSection();
	MenuBuilder.BeginSection("SectionHook", LOCTEXT("SectionNameB", "Section B"));

	MenuBuilder.AddSubMenu(
		LOCTEXT("PulldownMenuTitle3", "Sub Menu 3"),
		LOCTEXT("PulldownMenuToolTip3", "A description of this submenu is given here."),
		FNewMenuDelegate::CreateRaw(this, &FSamplePluginModule::HandleRegisterSubMenu3)
	);

	MenuBuilder.EndSection();
}

```
今回は試しに3つのメニューを追加しています。
`AddSubMenu`関数については上記の`AddPullDownMenu`関数と使い方は同じです。

プルダウンメニュー内でセクションを分ける場合は`BeginSection`関数と`EndSection`関数を使います。
`BeginSection`関数をセクション名を指定して呼び出してから次に`EndSection`関数が呼ばれるところまでが1セクションとなります。こちらは必ず必要なものではありません。

最後にサブメニュー内に処理を呼び出すメニューを追加していきます。
[SamplePlugin.cpp]
(https://github.com/Naotsun19B/ExtendMenuSample/blob/6b14107da63346b85c953d3af915541c8af20d95/Plugins/SamplePlugin/Source/SamplePlugin/Private/SamplePlugin.cpp#L109)

```SamplePlugin.cpp
void FSamplePluginModule::HandleRegisterSubMenu2(FMenuBuilder& MenuBuilder)
{
	MenuBuilder.AddMenuEntry(
		LOCTEXT("MenuTitle2_1", "Sample Command 3"),
		LOCTEXT("ToolTip2_1", "A description of this command is given here."),
		FSlateIcon(),
		FUIAction(FExecuteAction::CreateLambda([]()
	{
		GEngine->AddOnScreenDebugMessage(0, 3.f, FColor::Green, TEXT("Execute Sample Command 3"));
	}))
	);
}
```
メニューの追加には`AddMenuEntry`関数を使用します。
第一引数と第二引数はこれまでと同じく表示名と説明文です。
第三引数はメニューの左に表示されるアイコンを指定します。今回は何も表示していません。
第四引数もこれまで同様、メニューが選択された際に呼ばれるコールバック関数です。

`AddMenuEntry`関数を`OnPulldownMenuExtension`関数内で呼ぶことでサブメニューを追加せずにメニューを追加することもできます。

#おわりに
そもそもメニューを追加する拡張についての記事が少ない上に、Windowなど既存のプルダウンメニューに追加する記事が多かったので独自メニューを追加する方法はHoudiniEngineのプラグインをカンニングしました。

この記事で紹介したプロジェクトは以下でダウンロードできます。
https://github.com/Naotsun19B/ExtendMenuSample