独自のタブをエディタのメニューから呼び出す方法

#はじめに
この記事は以下の記事の続きになりますので、こちらの記事を読んでいる前提で書きますのでご了承ください...。
[UnrealC++でレベルエディタに独自メニューを追加する](https://qiita.com/Naotsun/items/7954d18304d2d09f0bf9)

前回の記事ではレベルエディタ上部に独自のメニューを追加し、そこから機能を呼び出す方法をご紹介しました。
今回は、WindowsのメニューにあるWorld OutlinerやWorld Settingsのタブの様にメニューのボタンを押したらタブが開き、タブが開いている時はメニューの左側にチェックマークが付くようにする方法をご紹介します。
![1.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325493/ca08c67d-aa90-fb67-28be-dc00cf6d485b.png)

今回の記事で使用するプロジェクトはUE4.27ですが、UE5でも同様に動作します。

#やり方
まずは今回作成する機能で使うモジュールを追加します。

[SamplePlugin.Build.cs](https://github.com/Naotsun19B/ExtendMenuSample/blob/2f0ada46c18372d88d25eada203649d5130d6b33/Plugins/SamplePlugin/Source/SamplePlugin/SamplePlugin.Build.cs#L27)

``` 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",
				"WorkspaceMenuStructure", // 追加.
			}
			);
	}
```

続いて先程追加したモジュールの必要なヘッダーをインクルードします。

[SamplePlugin.cpp](https://github.com/Naotsun19B/ExtendMenuSample/blob/2f0ada46c18372d88d25eada203649d5130d6b33/Plugins/SamplePlugin/Source/SamplePlugin/Private/SamplePlugin.cpp#L7)

``` SamplePlugin.cpp
#include "WorkspaceMenuStructure.h"
#include "Widgets/Docking/SDockTab.h"
```

次に今回追加する独自タブの登録をしていきます。
タブ固有のIDを`FName`で定義します。今回はサンプルということで`タブのID = タブの名前`で作成していきます。

[SamplePlugin.cpp](https://github.com/Naotsun19B/ExtendMenuSample/blob/2f0ada46c18372d88d25eada203649d5130d6b33/Plugins/SamplePlugin/Source/SamplePlugin/Private/SamplePlugin.cpp#L15)

``` SamplePlugin.cpp
namespace SamplePluginDefine
{
	static const FName LevelEditorName = TEXT("LevelEditor");
	static const FName TabName = TEXT("TestTab");
}
```

とりあえず、それっぽいnamespaceで囲いタブのID兼名前となる`FName`の変数を定義します。
（前回の記事ではLevelEditorの名前をマクロで定義していましたが、タブのIDと同じスタイルに修正しました）

次にGlobalTabManagerにタブを登録します。
先程定義したタブのIDとタブのSlateを生成する関数を指定します。
また、戻り値でタブのスポナーの情報を示す[`FTabSpawnerEntry`](https://docs.unrealengine.com/4.26/en-US/API/Runtime/Slate/Framework/Docking/FTabSpawnerEntry/)が貰えるのでそこにタブの左上に表示される名前やツールチップなどを設定していきます。

[SamplePlugin.cpp](https://github.com/Naotsun19B/ExtendMenuSample/blob/2f0ada46c18372d88d25eada203649d5130d6b33/Plugins/SamplePlugin/Source/SamplePlugin/Private/SamplePlugin.cpp#L38)

``` SamplePlugin.cpp
void FSamplePluginModule::StartupModule()
{
	// ~~~

	const TSharedRef<FGlobalTabmanager> GlobalTabManager = FGlobalTabmanager::Get();
	GlobalTabManager->RegisterTabSpawner(
		SamplePluginDefine::TabName,
		FOnSpawnTab::CreateRaw(this, &FSamplePluginModule::HandleRegisterTabSpawner)
	)
	.SetDisplayName(FText::FromName(SamplePluginDefine::TabName))
	.SetGroup(WorkspaceMenu::GetMenuStructure().GetLevelEditorCategory())
	// .SetIcon(< const FSlateBrush*の形でアイコン画像を指定 >)
	.SetTooltipText(LOCTEXT("TabTooltipText", "This is a test tab."));
}
```

余談ですが、コメントにもある通りタブの左上のアイコンを変更したい場合はSetIconでアイコンの画像を指定します。

ここで重要なのが`SetGroup`で設定しているワークスペースのカテゴリです。
後述する間違った方法で実装する場合はこの設定が無くても動作しますが、今回ご紹介する正規？の実装では必ず設定する必要があります。
こちらは`FWorkspaceMenuStructureModule`のコメントでも触れられています。

[Engine\Source\Editor\WorkspaceMenuStructure\Public\WorkspaceMenuStructureModule.h](https://github.com/EpicGames/UnrealEngine/blob/d94b38ae3446da52224bedd2568c078f828b4039/Engine/Source/Editor/WorkspaceMenuStructure/Public/WorkspaceMenuStructureModule.h#L16)

```WorkspaceMenuStructureModule.h
/**
 * The module that defines a structure of the workspace menu.
 * Tab spawners place themselves into one of the categories/groups
 * in this structure upon registration.
 */
class FWorkspaceMenuStructureModule : public IModuleInterface
```

タブの登録を行ったので当然登録解除もペアで実装する必要があります。
特に難しいことをする必要はなく、タブのIDを指定して登録の解除を行います。

[SamplePlugin.cpp](https://github.com/Naotsun19B/ExtendMenuSample/blob/2f0ada46c18372d88d25eada203649d5130d6b33/Plugins/SamplePlugin/Source/SamplePlugin/Private/SamplePlugin.cpp#L50)

``` SamplePlugin.cpp
void FSamplePluginModule::ShutdownModule()
{
	const TSharedRef<FGlobalTabmanager> GlobalTabManager = FGlobalTabmanager::Get();
	GlobalTabManager->UnregisterTabSpawner(SamplePluginDefine::TabName);
	
	// ~~~
}
```

次にタブを生成する部分を実装していきます。
タブを登録する部分でデリゲートに指定していた関数を追加していきます。

[SamplePlugin.h](https://github.com/Naotsun19B/ExtendMenuSample/blob/2f0ada46c18372d88d25eada203649d5130d6b33/Plugins/SamplePlugin/Source/SamplePlugin/Public/SamplePlugin.h#L33)

```SamplePlugin.h
	// タブが生成されるときに呼ばれる.
	TSharedRef<SDockTab> HandleRegisterTabSpawner(const FSpawnTabArgs& TabSpawnArgs);
```

[SamplePlugin.cpp](https://github.com/Naotsun19B/ExtendMenuSample/blob/2f0ada46c18372d88d25eada203649d5130d6b33/Plugins/SamplePlugin/Source/SamplePlugin/Private/SamplePlugin.cpp#L166)

```SamplePlugin.cpp
TSharedRef<SDockTab> FSamplePluginModule::HandleRegisterTabSpawner(const FSpawnTabArgs& TabSpawnArgs)
{
	TSharedRef<SDockTab> SpawnedTab = SNew(SDockTab)
		.TabRole(ETabRole::NomadTab)
		//.Icon(< const FSlateBrush*の形でアイコン画像を指定 >)
		[
			SNew(STextBlock)
			.Text(LOCTEXT("TestTabText", "This is a test tab."))
		];
		
	return SpawnedTab;
}
```

[`SDockTab`](https://docs.unrealengine.com/4.27/en-US/API/Runtime/Slate/Widgets/Docking/SDockTab/)がタブの実体？でこの子ウィジェットに表示したいウィジェットを設定します。（もちろんSlateで）
TablRoleにはこのタブをどのように扱うかを指定します。今回はどのタブともドッキングできるように`NomadTab`を指定します。
タブの左上のアイコンを変更したい場合はコメントにある通りアイコン画像を`const FSlateBrush*`の型で指定します。

この時点でタブの登録自体は完了しているので、[`FGlobalTabmanager::TryInvokeTab`](https://docs.unrealengine.com/4.27/en-US/API/Runtime/Slate/Framework/Docking/FTabManager/TryInvokeTab/)にタブのIDを指定して呼び出せばタブを表示することはできるようになりました。

最後に登録されたタブをメニューに追加する部分を実装します。
こちらもやることは簡単で[`FGlobalTabmanager::PopulateTabSpawnerMenu`](https://docs.unrealengine.com/4.27/en-US/API/Runtime/Slate/Framework/Docking/FTabManager/PopulateTabSpawnerMenu/2/)を拡張対象のMenuBuilderとタブのIDを指定して呼び出すだけでメニューへの登録は完了です。

[SamplePlugin.cpp](https://github.com/Naotsun19B/ExtendMenuSample/blob/2f0ada46c18372d88d25eada203649d5130d6b33/Plugins/SamplePlugin/Source/SamplePlugin/Private/SamplePlugin.cpp#L101)

```SamplePlugin.cpp
void FSamplePluginModule::OnPulldownMenuExtension(FMenuBuilder& MenuBuilder)
{
	// ~~~

	MenuBuilder.BeginSection(TEXT("SectionHook"), LOCTEXT("SectionNameC", "Section C"));
	
	const TSharedRef<FGlobalTabmanager> GlobalTabManager = FGlobalTabmanager::Get();
	GlobalTabManager->PopulateTabSpawnerMenu(MenuBuilder, SamplePluginDefine::TabName);

	MenuBuilder.EndSection();
}
```

ここまで実装してビルドし、エディタを起動すると画像のように独自のタブをエディタメニューから呼び出せるようになりました。
（ちゃんとタブが開いている時はメニューの横にチェックマークが付きます！！）

![2.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325493/3b987d52-74a7-edc3-abc5-632bf94cc7a0.png)
![3.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325493/6a566722-8c28-5d8a-bb8f-4ab35dcbfdc7.png)

#よくやる間違い？
``` Mistake.cpp
	MenuBuilder.AddMenuEntry(
		LOCTEXT("MenuTitle1_1", "Sample Command 1"),
		LOCTEXT("ToolTip1_1", "A description of this command is given here."),
		FSlateIcon(),
		FUIAction(FExecuteAction::CreateLambda([]()
		{
			const TSharedRef<FGlobalTabmanager> GlobalTabManager = FGlobalTabmanager::Get();
			GlobalTabManager->TryInvokeTab(SamplePluginDefine::TabName);
		}))
	);
```
このように前回の記事でご紹介した方法でGlobalTabManagerからタブを呼び出しても同じような機能を作成できますが、タブが出ている時にメニューの左側にチェックマークがでません。

#おわりに
長々と実装方法をご紹介しましたが、この記事を要約するとチェックマークを出すのがめちゃくちゃめんどくさいということです。
エディタメニューの実装とタブの登録方法を知っていればタブの呼び出し機能自体は実装でき、おそらく使用者目線ではチェックマークがあるかないかの違いしかありません。（内部的には色々と不都合がありそう...）
意外とこのことについて触れている記事が少なかったので、今後チェックマークがでなくて困っている人のお役に立てれば幸いです。

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

明日は、@ntenten_Qさんの「Niagaraの"Age"について」です！