まえがき
今回解説するのはSlateを使ったシンプルなアセットリストの作り方になります。
Unreal Engine(UE)にまだ不慣れなのにEditor Utility Widget(EUW)では出来ないような機能と立派なUIをエディタ内に独自に実装することを求めらるような、もしかしたらそんな人いないかもしれないけどそういうニッチなプログラマーをターゲットにしています。
いえ。。最近初めてSlateを使って大掛かりなものを作りましたので、その初学で学んだことをアウトプットしていこうという事です。
さて、今回はアセットの列挙に特化したものを作りますので、コンテンツブラウザなどエディタ内の様々なところに組み込まれているAssetPickerという機能を使います。
せっかくなのでAssetPickerの隣にDetailViewをつけて、選択したアセットのプロパティを直接編集できるようにしておきたいと思います。単なるリストならコンテンツブラウザでええやんってなりますしね。
今回は非常にシンプルなものを作りますが、いずれ機会があればもっと複雑なものを紹介できるかもしれません。なので許してえ。。
環境
本記事を作るに当たり使用した環境は下記の通りです
Visual Studio 2022 17.7.4
Unreal Engine 5.3.1
準備
Slateの基本的な作り方や導入に関しては公式のクイックスタートガイドを見るのが一番手っ取り早いので、そちらを参考にしてください。
本記事も、このガイドでプラグインを作った状態から作業を始める想定です。
まず適当にゲームのサードパーソンテンプレートを選び、C++プロジェクトにしてスターターコンテンツにチェックを入れて適当なプロジェクト名を入力してプロジェクトを作ります。
手元の環境ではプロジェクト名を「MySlateProject」としました。
ガイドに従いプラグインを作ります。
手元の環境では「MySlatePlugin」としました。
最初のプラグインのサンプルが表示されるところまで確認します。
(ガイドに従いそれ以降の対応を行って頂いても問題ありません)
次に今回作成するアセットリストのクラスを実装するソースファイルを作ります。
手元の環境では「MyAssetList.cpp(.h)」を作りました。
#pragma once
#include "Widgets/Docking/SDockTab.h"
class SMyAssetList : public SDockTab
{
public:
//インスタンス生成時に呼ばれる
void Construct(const FArguments& InArgs);
}
#include "MyAssetList.h"
void SMyAssetList::Construct(const FArguments& InArgs)
{
SDockTab::Construct(SDockTab::FArguments()
.TabRole(ETabRole::NomadTab)
[
SNew(STextBlock)
.Text(FText::FromString(TEXT("Hello, World!")))
]
);
}
関数が作れたらプラグインのソースファイル(手元だとMySlatePlugin.cpp)を開いて、
OnSpawnPluginTab関数を書きかえます。
TSharedRef<SDockTab> FMySlatePluginModule::OnSpawnPluginTab(const FSpawnTabArgs& SpawnTabArgs)
{
//アセットリストを生成して返すだけ(MyAssetList.hのincludeもお忘れなきよう)
return SNew(SMyAssetList);
}
プラグインを実行すると新しいタブが表示され「Hello, World!」と表示されるかと思います。
※必ずしもMyAssetList.cpp(.h)を作る必要はありませんが、僕が分かりやすいので作っています
AssetPicker
AssetPickerは、FContentBrowserSingleton::CreateAssetPicker関数で生成できます。
AssetPicker本体のクラスはエンジン側のプライベートなクラスのため、直接クラスインスタンスを生成することは出来ません。
まず作る
AssetPickerを作るにはFAssetPickerConfigというデータを作る必要がありますので、AssetPickerを作る処理を関数化しておきます。その方が後ほどSlateを組み立てるのが多少楽になるためです。
class SMyAssetList : public SDockTab
{
//(略)
private:
//AssetPickerを生成します
TSharedRef<SWidget> CreateAssetPicker();
MyAssetList.cppにCreateAssetPicker関数を実装し、Construct関数を書き換えます。
TSharedRef<SWidget> SMyAssetList::CreateAssetPicker()
{
//CreateAssetPicker関数を使いたいので、ContentBrowserModuleをロードします
//ContentBrowserModuleを使うには、~.Build.csにて依存モジュールにContentBrowserを追加します
//ContentBrowserModule.hのインクルードも追加してください
FContentBrowserModule& ContentBrowserModule = FModuleManager::Get().LoadModuleChecked<FContentBrowserModule>(TEXT("ContentBrowser"));
//AssetPickerを生成するときの設定です
//AssetPickerは、要するにコンテンツブラウザのアセット表示部分です
//今回はリストにしたいのでInitialAssetViewTypeはColumnにします
//FAssetPickerConfigを使うには、IContentBrowserSingleton.hのインクルードが必要です
FAssetPickerConfig Config;
Config.InitialAssetViewType = EAssetViewType::Column;
//AssetPickerを生成します
return ContentBrowserModule.Get().CreateAssetPicker(Config);
}
void SMyAssetList::Construct(const FArguments& InArgs)
{
SDockTab::Construct(SDockTab::FArguments()
.TabRole(ETabRole::NomadTab)
[
//後ほどAssetPickerの隣にDetailViewを表示するので、SHorizontalBoxを使います
SNew(SHorizontalBox)
+SHorizontalBox::Slot() .Padding(FMargin(3.0f))
[
CreateAssetPicker()
]
]
);
}
プラグインを実行すると下図のようなタブが出現するかと思います。
この状態ではリスト上にすべてのアセットが列挙され、すべてのカラムが表示されているため非常に見にくくなっており、使えたものではありません。
列挙される項目にフィルタを掛ける
まず列挙されるアセットにフィルタをかけます。
CreateAssetPicker関数内で設定しているFAssetPickerConfigを編集し、アイテムフィルタを設定します。
フィルタはSetFilterDelegatesというデリゲートを設定することで対応します。
#include "ContentBrowserDelegates.h"
//(略)
class SMyAssetList : public SDockTab
{
//(略)
private:
FSetARFilterDelegate SetFilterDelegate;
//(略)
}
TSharedRef<SWidget> SMyAssetList::CreateAssetPicker()
{
//(略)
FAssetPickerConfig Config;
Config.InitialAssetViewType = EAssetViewType::Column;
//SetFilterDelegatesというデリゲートを設定してフィルタをかけられるようにします
Config.SetFilterDelegates.Add(&SetFilterDelegate)
//(略)
}
デリゲートの準備ができたら、それを使ってフィルタをかけます。
アセットリストを作るためのInitializeAssetList関数を実装し、AssetPicker生成後に呼び出します。
//(略)
class SMyAssetList : public SDockTab
{
//(略)
//アセットリストを初期化します
void InitializeAssetList();
void SMyAssetList::InitializeAssetList()
{
//GetAssets関数を使いたいので、AssetRegistryModuleをロードします
//AssetRegistryModuleを使うには、~.Build.csにて依存モジュールにAssetRegistryを追加します
//AssetRegistry/AssetRegistryModule.hのインクルードも追加してください
const FAssetRegistryModule& AssetRegisterModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry");
const IAssetRegistry& AssetRegistry = AssetRegisterModule.Get();
FARFilter Filter;
//参照するフォルダを指定できます。配列なので複数フォルダの指定も可能です
Filter.PackagePaths.Add(FName("/Game/"));
//PackagePathsで指定したフォルダ以下のフォルダも再帰的に参照するならtrueにします
Filter.bRecursivePaths = true;
//参照するアセットを特定アセットに絞るならClassPathsを指定します
//今回はメッシュアセットに限定しています
Filter.ClassPaths.Add(USkeletalMesh::StaticClass()->GetClassPathName());
Filter.ClassPaths.Add(UStaticMesh::StaticClass()->GetClassPathName());
//設定した条件のアセットデータ配列を取得します
TArray<FAssetData> DataAssets;
AssetRegistry.GetAssets(Filter, DataAssets);
//取得したアセットデータ配列から、リストに列挙するアセットを取り出します
Filter.Clear();
for (FAssetData& AssetData : DataAssets)
{
Filter.PackageNames.Add(AssetData.PackageName);
}
//もしアセットがないなら、関係ないパスを指定してリストが表示されないようにします
if (Filter.PackageNames.IsEmpty())
{
Filter.PackageNames.Add("/Temp/FakePackageNameToMakeNothingShowUp");
}
//AssetPickerに設定したフィルタのデリゲートを実行し、リストを更新します
SetFilterDelegate.Execute(Filter);
}
void SMyAssetList::Construct(const FArguments& InArgs)
{
SDockTab::Construct(SDockTab::FArguments()
//(略)
//AssetPickerを作ったあとでフィルターを作る関数を呼び出します
InitializeAssetList();
}
ここ迄で、下図のような感じになります。
列挙されるアセットの数が166から10に減りました。
スケルタルメッシュとスタティックメッシュのみに変わっています。
まだ見づらいですが、フィルタは出来ました。
最初から表示するカラムを制限する
ここまでの状態だとリストが非常に見にくい状態ですので、最初から表示されるカラムを制限したいと思います。
カラムの初期表示状態を設定するには、FAssetPickerConfigのHiddenColumnNames配列に非表示するカラムを追加します。
TSharedRef<SWidget> SMyAssetList::CreateAssetPicker()
{
//(略)
FAssetPickerConfig Config;
//(略)
//最初から非表示にしておきたいカラムを指定します
//Config.HiddenColumnNames.Add(TEXT("RevisionControl"));
//Config.HiddenColumnNames.Add(TEXT("Class"));
Config.HiddenColumnNames.Add(TEXT("Path"));
Config.HiddenColumnNames.Add(TEXT("ItemDiskSize"));
Config.HiddenColumnNames.Add(TEXT("HasVirtualizedData"));
//Config.HiddenColumnNames.Add(TEXT("Vertices"));
Config.HiddenColumnNames.Add(TEXT("Triangles"));
Config.HiddenColumnNames.Add(TEXT("MorphTargets"));
Config.HiddenColumnNames.Add(TEXT("Skeleton"));
Config.HiddenColumnNames.Add(TEXT("PhysicsAsset"));
Config.HiddenColumnNames.Add(TEXT("NeverStream"));
//Config.HiddenColumnNames.Add(TEXT("Bones"));
Config.HiddenColumnNames.Add(TEXT("ShadowPhysicsAsset"));
Config.HiddenColumnNames.Add(TEXT("LODSettings"));
Config.HiddenColumnNames.Add(TEXT("SkinWeightProfiles"));
//Config.HiddenColumnNames.Add(TEXT("LODs"));
Config.HiddenColumnNames.Add(TEXT("MaxBoneInfluences"));
//(略)
}
この様にしてちょっとお邪魔なカラムを非表示にしてあげることで、下図のようにスッキリさせることが出来ます。
カラムの内容は独自の内容を追加することが出来ます。
カスタムカラムの設定になるのですが、応用的な使い方なのでカスタムカラムについては後述します。
カラムの表示設定を保存する
カラムの表示設定はカラムを右クリックして出てくるメニューで設定することが出来ます。
この設定は保存して次回ツールを開いた際にその内容を反映することが出来ます。
保存するためには、FAssetPickerConfigのSaveSettingsNameに任意の文字列を指定します。
TSharedRef<SWidget> SMyAssetList::CreateAssetPicker()
{
//(略)
FAssetPickerConfig Config;
//(略)
//ユーザーごとのEditorPerProjectUserSettings.iniファイルの
//ContentBrowserセクションにに指定した名前で情報が保存されます
Config.SaveSettingsName = TEXT("MyToolSettings");
//(略)
}
EditorPerProjectUserSettings.iniファイルですが、プロジェクトフォルダ以下の
Savedフォルダ内にあるConfigフォルダのWindowsEditorフォルダに作られます。
(例)
MySlateProject\Saved\Config\WindowsEditor\EditorPerProjectUserSettings.ini
ここまでで、とりあえずAssetPickerが作れるようになったかと思います。
AssetPickerはまだカスタムカラムやコンテキストメニューなど説明しますが、一旦はDetail Viewを説明します。
DetailView
まずは作る
AssetPickerと同様にDetailViewを生成する関数を用意して、AssetPickerの隣に配置します。
class SMyAssetList : public SDockTab
{
//(略)
//DetailViewを生成します
TSharedRef<SWidget> CreateDetailView();
TSharedRef<SWidget> SMyAssetList::CreateDetailView()
{
FPropertyEditorModule& PropertyEditor = FModuleManager::LoadModuleChecked<FPropertyEditorModule>("PropertyEditor");
FDetailsViewArgs Args;
Args.bShowOptions = false;
Args.bAllowSearch = false;
Args.bShowPropertyMatrixButton = false;
Args.bUpdatesFromSelection = false;
Args.bLockable = false;
Args.NameAreaSettings = FDetailsViewArgs::HideNameArea;
Args.ViewIdentifier = "Create Control Asset";
DetailView = PropertyEditor.CreateDetailView(Args);
return SNew(SBox).HAlign(HAlign_Center).VAlign(VAlign_Center)
[
DetailView.ToSharedRef()
];
}
void SMyAssetList::Construct(const FArguments& InArgs)
{
SDockTab::Construct(SDockTab::FArguments()
.TabRole(ETabRole::NomadTab)
[
//AssetPickerの隣にDetailViewを表示するので、SHorizontalBoxを使います
SNew(SHorizontalBox)
+SHorizontalBox::Slot() .Padding(FMargin(3.0f))
[
CreateAssetPicker()
]
+SHorizontalBox::Slot() .Padding(FMargin(3.0f))
[
CreateDetailView()
]
]
);
}
ここ迄のコードをビルドしてエディタを起動し、プラグインを実行すると下図のような感じでDetailViewが出現するかと思います。
なんにも表示されとらへんやんけと思われたかと思いますが、実はあります。
詳細を表示する対象が指定されていないので、このような見た目になっています。
AssetPickerで選んだアセットの詳細を表示する
AssetPicker側に項目選択時のデリゲートを登録します。
その前に登録する関数を作ります。
class SMyAssetList : public SDockTab
{
//(略)
//アセットを選択した時の処理
void OnAssetSelected(const FAssetData& InAssetData);
void SMyAssetList::OnAssetSelected(const FAssetData& InAssetData)
{
//ここに後で処理を入れます
}
この関数を、OnAssetSelectedデリゲートに登録します。
TSharedRef<SWidget> SMyAssetList::CreateAssetPicker()
{
//(略)
FAssetPickerConfig Config;
//アセットを選択したときに呼ばれるデリゲート
Config.OnAssetSelected = FOnAssetSelected::CreateSP(this, &SMyAssetList::OnAssetSelected);
OnAssetSelected関数が呼ばれることを確認できたら、選択した項目をDetailViewにわたす処理を実装します。
void SMyAssetList::OnAssetSelected(const FAssetData& InAssetData)
{
//GetAsset関数内でアセットのロードが走るのでご注意ください
DetailView->SetObject(InAssetData.GetAsset());
}
結果、選択したアセットの詳細がDetailViewに表示されます。
表示されているプロパティは編集することが出来ます。
まあプロパティマトリクスを使えば同様の機能を提供出来るのですが、拡張性がない(エンジン改造すればいくらでも出来ますが)ところが結構痛いです。またListViewを使って作ることも出来ますが、そこまで時間をかけられない時やそこまでの機能を求めていない時にはAssetPickerがちょうどいいのかなと思います。
Unreal Engineの基本機能はどれも優秀なので、プロジェクトで求められる機能に応じて必要な機能を組み合わせて使うのが良いかと思います。
AssetPickerその他
AssetPickerやListViewを使うなら、独自のカラムの追加や項目を右クリックした時のコンテキストメニューが欲しくなりますので、作り方を簡単に説明しておきたいと思います。
カスタムカラム
Unreal Engine既存のアセットなら大抵のカラムは用意されているので、主にオリジナルのデータアセットを扱う場合にはカスタムカラムが重宝します。
カスタムカラムを作るにはまず、カラムに表示する文字列を取得する関数と、ソートに使う値を取得する関数を作ります。
class SMyAssetList : public SDockTab
{
//(略)
//カスタムカラムの内部処理やソートに使用する値を文字列で取得します
FString GetStringValueForCustomColumn(FAssetData& AssetData, FName ColumnName) const;
//カスタムカラムの表示に使用するテキストを取得します
FText GetDisplayTextForCustomColumn(FAssetData& AssetData, FName ColumnName) const;
FString SMyAssetList::GetStringValueForCustomColumn(FAssetData& AssetData, FName ColumnName) const
{
FString OutValue;
return OutValue;
}
FText SMyAssetList::GetDisplayTextForCustomColumn(FAssetData& AssetData, FName ColumnName) const
{
FText OutValue;
OutValue = FText::FromString(TEXT("Sample"));
return OutValue;
}
作成した関数は用途に応じて実装してください。上記の例は有用な値は返していません。
次に作成した関数をAssetPicker作成時の設定データに登録して渡します。
TSharedRef<SWidget> SMyAssetList::CreateAssetPicker()
{
//(略)
FAssetPickerConfig Config;
// カスタムカラムの追加
Config.CustomColumns.Emplace(
FName(TEXT("ColumnTag")), //カラムの内部名
FText::FromString(TEXT("ColumnName")), //カラムの表示名
FText::FromString(TEXT("Tool tip")), //カラムのツールチップテキスト
UObject::FAssetRegistryTag::TT_Alphabetical, //カラムタイプ。ソートで使用します
FOnGetCustomAssetColumnData::CreateSP(this, &SMyAssetList::GetStringValueForCustomColumn), //カラムの内部値の取得関数
FOnGetCustomAssetColumnDisplayText::CreateSP(this, &SMyAssetList::GetDisplayTextForCustomColumn) //カラムの表示テキストの取得関数
);
第4引数のカラムタイプは、ソート時に値をどう扱ってソートするかを指定します。
上記のTT_Alphabeticalならアルファベット順でソートします。TT_Numericalなら値順でソートします。
追加したカスタムカラムが表示されるのが確認できるかと思います。
今回は独自アセットを作らなかったのでだいぶフワッとした説明になりましたが、独自アセットを使う場合には活用してみてください。
コンテキストメニュー
コンテキストメニューはアセットを右クリックしたときに表示される簡易メニューみたいなアレです。
みなさんも普段からよく使われているかと思います。
AssetPickerでコンテキストメニューを扱うには、自身でメニューを作ってあげるひつようがあります。
地味に面倒ではありますが操作性が向上することもありますので必要に応じて作ってみてください。
まずはコンテキストメニューを作る関数を作ります。
class SMyAssetList : public SDockTab
{
//(略)
//コンテキストメニューを作ります
TSharedPtr<SWidget> OnGetAssetContextMenu(const TArray<FAssetData>& SelectedAssets);
//(略)
//コンテキストメニューで定義したコマンドのデリゲートを保持するやつ
TSharedPtr<FUICommandList> Commands;
TSharedPtr<SWidget> SMyAssetList::OnGetAssetContextMenu(const TArray<FAssetData>& SelectedAssets)
{
FMenuBuilder MenuBuilder(/*bInShouldCloseWindowAfterMenuSelection=*/ true, Commands);
MenuBuilder.BeginSection(TEXT("Asset"), NSLOCTEXT("ReferenceViewerSchema", "AssetSectionLabel", "Asset"));
{
//後ほどここにメニュー項目を追加します
}
MenuBuilder.EndSection();
return MenuBuilder.MakeWidget();
}
関数が出来たら、AssetPickerのConfigでOnGetAssetContextMenuを設定します。
TSharedRef<SWidget> SMyAssetList::CreateAssetPicker()
{
//(略)
FAssetPickerConfig Config;
//アセットを右クリックしたときに開くコンテキストメニューを取得するデリゲートを設定します
Config.OnGetAssetContextMenu = FOnGetAssetContextMenu::CreateSP(this, &SMyAssetList::OnGetAssetContextMenu);
ここまででコンテキストメニューの登録は出来ていますが、まだメニューの中身(項目)がないので右クリックしても何も起きません。
項目の内容と処理を実装していきます。
まずはメニュー項目に必要な2つの関数を作ります。
class SMyAssetList : public SDockTab
{
//(略)
//コンテキストメニューの項目の処理が実行できるかどうか
bool CanContextSampleExecuteAction() const;
//コンテキストメニューの項目の処理を実行します
void ContextSampleExecuteAction() const;
bool SMyAssetList::CanContextSampleExecuteAction() const
{
//コンテキストメニューからアクションが実行出来るならtrueを返します。実行出来ないならfalseを返します
//falseを返している時はメニューの項目はグレーアウトされてクリック出来なくなります
return true;
}
void SMyAssetList::ContextSampleExecuteAction() const
{
//コンテキストメニューで項目がクリックされた際に実行するアクション(処理)を実装します
//Ex.アセットを開く、アセットを保存する等
}
関数を作ったら、OnGetAssetContextMenu関数内でメニュー項目を作ります。
TSharedPtr<SWidget> SMyAssetList::OnGetAssetContextMenu(const TArray<FAssetData>& SelectedAssets)
{
FMenuBuilder MenuBuilder(/*bInShouldCloseWindowAfterMenuSelection=*/ true, Commands);
MenuBuilder.BeginSection(TEXT("Asset"), NSLOCTEXT("ReferenceViewerSchema", "AssetSectionLabel", "Asset"));
{
MenuBuilder.AddMenuEntry(
FText::FromString(TEXT("Sample")), //項目名
FText::FromString(TEXT("Sample Tool tip")), //ツールTipテキスト
FSlateIcon(FAppStyle::GetAppStyleSetName(), "LevelEditor.Save"), //項目名の左隣に表示するアイコン。使わない場合はFSlateIcon()
FUIAction(
FExecuteAction::CreateSP(this, &SMyAssetList::ContextSampleExecuteAction), //項目の処理が実行できるかどうか
FCanExecuteAction::CreateSP(this, &SMyAssetList::CanContextSampleExecuteAction) //項目の処理が実行する
)
);
}
MenuBuilder.EndSection();
return MenuBuilder.MakeWidget();
}
これでサンプルのメニューが開けるようになっていると思います。
AssetPickerに列挙されている項目を右クリックすると下図のようなコンテキストメニューが開きます。
汎用コマンドを追加することも出来ます。
TSharedPtr<SWidget> SMyAssetList::OnGetAssetContextMenu(const TArray<FAssetData>& SelectedAssets)
{
FMenuBuilder MenuBuilder(/*bInShouldCloseWindowAfterMenuSelection=*/ true, Commands);
//(略)
MenuBuilder.BeginSection(FName(TEXT("Generic")), FText::FromString(TEXT("Generic")));
{
//定義済みの汎用的なコマンドを使うことも出来ます
//これらを使用するには"Framework/Commands/GenericCommands.h"のインクルードが必要です
MenuBuilder.AddMenuEntry(FGenericCommands::Get().Cut);
MenuBuilder.AddMenuEntry(FGenericCommands::Get().Copy);
MenuBuilder.AddMenuEntry(FGenericCommands::Get().Paste);
MenuBuilder.AddMenuEntry(FGenericCommands::Get().Duplicate);
MenuBuilder.AddMenuEntry(FGenericCommands::Get().Delete);
}
MenuBuilder.EndSection();
return MenuBuilder.MakeWidget();
}
結果コンテキストメニューには下図のような項目が追加されます。
最後にコンテキストメニューでコマンドを実行する時に、選択中のアセットに対して何かしら処理したいというとこが出てくると思いますので、選択中のアセットの取得方法について説明します。
AssetPickerのConfigで、GetCurrentSelectionDelegatesにデリゲートを登録することでデリゲートを経由して選択中のアセットを取得できるようになります。
DetailViewのときに用意したSetFilterDelegateと同様にデリゲートを定義します。
class SMyAssetList : public SDockTab
{
//(略)
//AssetPickerで選択中のアセットを取得するためのデリゲート
//ContentBrowserDelegates.hのインクルードが必要です
FGetCurrentSelectionDelegate GetCurrentSelectionDelegate;
定義したデリゲートを、AssetPickerのConfigに登録します。
TSharedRef<SWidget> SMyAssetList::CreateAssetPicker()
{
//(略)
FAssetPickerConfig Config;
//選択しているアセットを取得する際に使用するデリゲートを登録します
//登録したデリゲートを実行することでAssetPicker内で選択されているアセットの配列を得ることができます
Config.GetCurrentSelectionDelegates.Add(&GetCurrentSelectionDelegate);
GetCurrentSelectionDelegate.Execute関数を呼ぶことで、
選択中のアセットの配列(TArray)を得ることが出来ます。
選択中のアセットが得られるようになることで、言わずもがな選択中のアセットに対してのアクションを実行できるようになります。
さいごに
結構な長文になりましたが、基本的なところはおさえているかと思います。
AssetPickerでの対応はListViewにも転用出来ると思いますので、何かしらのお役に立てる事があれば幸いです。
今回のAssetPickerとDetailViewを基本としてパッと見は豪華な機能のツールを作りましたので、また機会があればこの他のSlate周りのノウハウも共有出来たらと思います。
まだまだSlateやエディタ機能への理解は足りていないところがありますので、間違っているところやより良いアプローチがありましたらコメントやご連絡など頂けますと大変助かります。
それではまたどこかで