【UE4】フォルダ選択ダイアログを作る

[1]:https://docs.unrealengine.com/en-US/API/Editor/ContentBrowser/IContentBrowserSingleton/index.html
[2]:https://docs.unrealengine.com/en-US/API/Editor/ContentBrowser/IContentBrowserSingleton/CreatePathPicker/index.html

uprojectの整理のためのEditor Utility Widgetを作成中に知ったことの備忘録。
フォルダを選択するUIがほしいが、いわゆる Blutility には公開されていない様子。
なので、なんとなく自作してみた。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/358170/8eb8bfb7-2ac9-5d34-2bcc-1444fdfd7e2a.png)

# IContentBrowserSingleton::CreatePathPicker()

[IContentBrowserSingleton][1] にある [CreatePathPicker][2] を使えばよい様子。
大雑把には、

1. FPathPickerConfig に必要な情報をセットする。
2. FContentBrowserModule を取得し、CreatePathPicker() を呼び出す。
3. Slate 製 SPathPicker が得られるので、それをダイアログウィンドウに貼り付ける。

という手順で作れた。

- FContentBrowserModule を使うので、ContentBrowserModule.h のインクルードと、ContentBrowser モジュールを .Build.cs に追加する必要がある。
- OK ボタンや Cancel ボタンはないので、Slate で自作する必要がある。

# ソースコード

## class ValidPath

- 存在しないパスを指定されたら、"/Game"に設定するだけのユーティリティクラス。
- UEditorAssetLibrary::DoesDirectoryExist() を使うので、EditorAssetLibrary.h のインクルードと、EditorScriptingUtilities モジュールの .Build.cs への追加が必要。

> アセット操作では AssetRegistry, AssetTools, ContentBrowser などのモジュールはよく使うものの、UEditorAssetLibrary の存在を忘れがち（私は）。
> 困ったら、なんかもう一つあったよね？(= UEditorAssetLibrary) を思い出すべし。
> ※UEditorAssetLibrary は、いわゆる Blutility にも公開されている関数群。

```C++:ValidPath
namespace assetutils
{
	class ValidPath
	{
	public:
		ValidPath(const FString& InPath)
		{
			if (UEditorAssetLibrary::DoesDirectoryExist(InPath))
			{
				Path = InPath;
			}
			else
			{
				UE_LOG(LogRinderonEditor, Warning, TEXT("assetutils::ValidPath()  InPath(%s) is not exist, \"/Game\" is used instead."), *InPath);
				Path = TEXT("/Game");
			}
		}
	public:
		const FString& Get() const { return Path; }
	private: // internal properties
		FString Path;
	}; // class ValidPath

} // namespace assetutils
```

## class SDirectoryDialog

パス選択ダイアログ本体。
Slate はろくにいじったことがないので、これで合っているか不安。

- SHorizontalBox buttonsBox は、OK ボタンと Cancel ボタンを持っている。
- SVerticalBox mainBox は、上側に SPathPicker を、下側に buttonsBox を持っている。

```C++:SDirectoryDialog
// directory selection dialog (slate)
class SDirectoryDialog : public SCompoundWidget
{
public:
	SLATE_BEGIN_ARGS(SDirectoryDialog) {}

	SLATE_END_ARGS()

public:
	SDirectoryDialog() {}
	virtual ~SDirectoryDialog() {}

	virtual void Construct(const FArguments& InArgs, const FString& InBasePath)
	{
		assetutils::ValidPath basePath(InBasePath);

		FPathPickerConfig config;
		config.DefaultPath = basePath.Get();
		config.OnPathSelected = FOnPathSelected::CreateRaw(this, &SDirectoryDialog::OnPathSelected);

		FContentBrowserModule& contentBrowserModule
			= FModuleManager::LoadModuleChecked<FContentBrowserModule>("ContentBrowser");
		TSharedPtr<SPathPicker> pathPicker = StaticCastSharedRef<SPathPicker>(
			contentBrowserModule.Get().CreatePathPicker(config));

		TSharedRef<SHorizontalBox> buttonsBox = SNew(SHorizontalBox)
			+ SHorizontalBox::Slot()
				.AutoWidth()
				.VAlign(VAlign_Bottom)
				.Padding(4.0f, 3.0f)
				[
					SNew(SButton)
					.Text(LOCTEXT("DirectoryPickerConfirmButton", "OK"))
					.ContentPadding(FMargin(8.0f, 2.0f))
					.IsEnabled(this, &SDirectoryDialog::IsConfirmButtonEnabled)
					.OnClicked(this, &SDirectoryDialog::OnConfirmButtonClicked)
				]
			+ SHorizontalBox::Slot()
				.AutoWidth()
				.VAlign(VAlign_Bottom)
				.Padding(4.0f, 3.0f)
				[
					SNew(SButton)
					.Text(LOCTEXT("DirectoryPickerCancelButton", "Cancel"))
					.ContentPadding(FMargin(8.0f, 2.0f))
					.OnClicked(this, &SDirectoryDialog::OnCancelButtonClicked)
				];

		TSharedRef<SVerticalBox> mainBox = SNew(SVerticalBox)
			+ SVerticalBox::Slot()
				.FillHeight(1.0f)
				.Padding(0.0f, 0.0f, 0.0f, 4.0f)
				[
					pathPicker.ToSharedRef()
				]
			+ SVerticalBox::Slot()
				.AutoHeight()
				.HAlign(HAlign_Fill)
				.Padding(0.0f)
				[
					buttonsBox
				];

		ChildSlot
			[
				mainBox
			];
	}

public: // public methods
	bool HasValidResult() const { return ConfirmedFlag && (!SelectedPath.IsEmpty()); }
	const FString& GetPath() const { return SelectedPath; }

private: // internal methods: utilities
	void CloseDialog()
	{
		TSharedPtr<SWindow> window = FSlateApplication::Get().FindWidgetWindow(AsShared());
		if (window.IsValid())
		{
			window->RequestDestroyWindow();
		}
	}

private: // internal methods: delegates
	bool IsConfirmButtonEnabled() const { return true; }

	FReply OnConfirmButtonClicked()
	{
		CloseDialog();
		ConfirmedFlag = true;
		return FReply::Handled();
	}

	FReply OnCancelButtonClicked()
	{
		CloseDialog();
		ConfirmedFlag = false;
		return FReply::Handled();
	}

	void OnPathSelected(const FString& InCurrentPath) { SelectedPath = InCurrentPath; }

private: // internal properties
	FString SelectedPath;
	bool ConfirmedFlag = false;
};
```

## BrowseForFolder() 関数

ダイアログを呼び出すための Blueprint Function Library の関数。

- IMainFrameModule のために、Interfaces/IMainFrameModule.h のインクルードと、MainFrame モジュールの .Build.cs への追加が必要。

```C++:BrowseForFolder
void URinderonEditorFunctionLibrary::BrowseForFolder(const FString& InBasePath,
    bool& OutSuccess, FString& OutPath)
{
	TSharedRef<SWindow> window = SNew(SWindow)
		.Title(LOCTEXT("BrowseForFolderTitle", "Browse Folders"))
		.ClientSize(FVector2D(320.0f, 320.0f))
		.SizingRule(ESizingRule::UserSized)
		.SupportsMaximize(false)
		.SupportsMinimize(false);

	TSharedRef<SDirectoryDialog> dialog = SNew(SDirectoryDialog, InBasePath);
	window->SetContent(dialog);

	IMainFrameModule& mainFrameModule
		= FModuleManager::LoadModuleChecked<IMainFrameModule>("MainFrame");
	const TSharedPtr<SWindow>& parentWindow = mainFrameModule.GetParentWindow();
	if (parentWindow.IsValid())
	{
		FSlateApplication::Get().AddModalWindow(window, parentWindow.ToSharedRef());
		if (dialog->HasValidResult())
		{
			OutSuccess = true;
			OutPath = dialog->GetPath();
			return;
		}
	}

	OutSuccess = false;
	OutPath.Empty();
	return;
}
```

# Editor Utility Widget からの呼び出し

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/358170/441796f7-6d80-baca-a878-57f39c8dee14.png)

非常にシンプルに使えて便利。