How to make a custom Wizard for Unreal Editor
21 Jun
Written By Roderick Kennedy
I wanted to create a wizard in the trueSKY Unreal plugin that would make it easier for users to add trueSKY to UE scenes. I was following this video where Epic's Michael Noland describes various ways to modify the Editor. So I made a custom Property Editor window with settings to select a sky sequence, create a TrueSkyLight etc.
But it didn't look very friendly. And implementing a wizard-style Apply button just put a button in amongst the other settings - not great. After some searching in the UE codebase, I discovered the SWizard class that Unreal Editor uses for its own wizards. Here's what you do:
1. Create a class derived from SCompoundWidget containing a TSharedPtr<SWizard>. Mine looks like this:
DECLARE_DELEGATE_FourParams( FOnTrueSkySetup, bool, ADirectionalLight* ,bool, UTrueSkySequenceAsset *);
#define S_DECLARE_CHECKBOX(name) \ bool name; \ ECheckBoxState Is##name##Checked() const { return name ? ECheckBoxState::Checked:ECheckBoxState::Unchecked;} \ void On##name##Changed(ECheckBoxState InCheckedState) {name=(InCheckedState==ECheckBoxState::Checked);}
class STrueSkySetupTool : public SCompoundWidget { public: SLATE_BEGIN_ARGS( STrueSkySetupTool ) :_CreateTrueSkyLight(false) ,_DirectionalLight(nullptr) ,_CreateDirectionalLight(nullptr) ,_Sequence(nullptr) {} / A TrueSkyLight actor performs real-time ambient lighting.*/ SLATE_ARGUMENT(bool,CreateTrueSkyLight) / TrueSKY can drive a directional light to provide sunlight and moonlight./ SLATE_ARGUMENT(ADirectionalLight,DirectionalLight) / If there's no directional light in the scene, you can create one with this checkbox.*/ SLATE_ARGUMENT(bool,CreateDirectionalLight) / The TrueSKY Sequence provides the weather state to render./ SLATE_ARGUMENT(UTrueSkySequenceAsset ,Sequence) / Event called when code is successfully added to the project */ SLATE_EVENT( FOnTrueSkySetup, OnTrueSkySetup ) SLATE_END_ARGS() / Constructs this widget with InArgs */ void Construct( const FArguments& InArgs );
/* Handler for when cancel is clicked / void CancelClicked();
/* Returns true if Finish is allowed / bool CanFinish() const;
/* Handler for when finish is clicked / void FinishClicked();
...
S_DECLARE_CHECKBOX(CreateTrueSkyLight) S_DECLARE_CHECKBOX(ShowAllSequences)
void SetupSequenceAssetItems();
void CloseContainingWindow(); private: /* The wizard widget / TSharedPtr MainWizard; FOnTrueSkySetup OnTrueSkySetup; ... };
The SLATE_ARGUMENT macros allow initialization of named parameters in this style:
TSharedRef TrueSkySetupTool = SNew(STrueSkySetupTool).OnTrueSkySetup(OnTrueSkySetup1).CreateTrueSkyLight(true);
etc. This is super-useful.
2. Create a callback for the wizard to execute:
FOnTrueSkySetup OnTrueSkySetupDelegate;
3. Create a window for the widget. This function is called when the menu option to start the wizard is selected:
void FTrueSkyEditorPlugin::OnAddSequence() { TrueSkySetupWindow = SNew(SWindow) .Title( NSLOCTEXT("InitializeTrueSky", "WindowTitle", "Initialize trueSKY") ) .ClientSize( FVector2D(600, 550) ) .SizingRule( ESizingRule::FixedSize ) .SupportsMinimize(false).SupportsMaximize(false); OnTrueSkySetupDelegate.BindRaw(this,&FTrueSkyEditorPlugin::OnTrueSkySetup); TSharedRef TrueSkySetupTool = SNew(STrueSkySetupTool).OnTrueSkySetup(OnTrueSkySetupDelegate); TrueSkySetupWindow->SetContent( TrueSkySetupTool );
If the main frame exists parent the window to it. The main frame should always exist...
TSharedPtr< SWindow > ParentWindow; if( FModuleManager::Get().IsModuleLoaded( "MainFrame" ) ) { IMainFrameModule& MainFrame = FModuleManager::GetModuleChecked( "MainFrame" ); ParentWindow = MainFrame.GetParentWindow(); }
bool modal=false; if (modal) { FSlateApplication::Get().AddModalWindow(TrueSkySetupWindow.ToSharedRef(), ParentWindow); } else if (ParentWindow.IsValid()) { FSlateApplication::Get().AddWindowAsNativeChild(TrueSkySetupWindow.ToSharedRef(), ParentWindow.ToSharedRef()); } else { FSlateApplication::Get().AddWindow(TrueSkySetupWindow.ToSharedRef()); } TrueSkySetupWindow->ShowWindow(); }
4. Implement the setup tool:
BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION void STrueSkySetupTool::Construct( const FArguments& InArgs ) { OnTrueSkySetup = InArgs._OnTrueSkySetup; CreateTrueSkyLight=InArgs._CreateTrueSkyLight; DirectionalLight=InArgs._DirectionalLight; Sequence=InArgs._Sequence; ...
The interface to build the actual UI is really interesting. By overloading the [] and + operators, Epic lets you specify the widget structure like so:
ChildSlot [ SNew(SBorder) .Padding(18) .BorderImage( FEditorStyle::GetBrush("Docking.Tab.ContentAreaBrush") ) [ SNew(SVerticalBox) +SVerticalBox::Slot() [ SAssignNew( MainWizard, SWizard) .ShowPageList(false) .CanFinish(this, &STrueSkySetupTool::CanFinish) .FinishButtonText( LOCTEXT("TrueSkyFinishButtonText", "Initialize") ) .OnCanceled(this, &STrueSkySetupTool::CancelClicked) .OnFinished(this, &STrueSkySetupTool::FinishClicked) .InitialPageIndex( 0) +SWizard::Page() [ SNew(SVerticalBox) +SVerticalBox::Slot() .AutoHeight() [ SNew(STextBlock) .TextStyle( FEditorStyle::Get(), "NewClassDialog.PageTitle" ) .Text( LOCTEXT( "WeatherStateTitle", "Choose a Sequence Asset" ) ) ] +SVerticalBox::Slot() .AutoHeight() .Padding(0) [ SNew(SHorizontalBox) +SHorizontalBox::Slot() .FillWidth(1.f) .VAlign(VAlign_Center) [ SNew(STextBlock) .Text(LOCTEXT("TrueSkySetupToolDesc", "Choose which weather sequence to use initially.") ) .AutoWrapText(true) .TextStyle(FEditorStyle::Get(), "NewClassDialog.ParentClassItemTitle") ] ] ] +SWizard::Page() [ ... ] ] ] ];
}
So by adding new +SWizard::Page() elements we add pages to the wizard.
5. Finally, implement the callback that the delegate calls when you click "Finish":
void FTrueSkyEditorPlugin::OnTrueSkySetup(bool CreateDirectionalLight, ADirectionalLight DirectionalLight,bool CreateTrueSkyLight,UTrueSkySequenceAsset Sequence) { ... }
The end result looks like this:
Full source for this is at our UE branch, (register at Simul to access).
How to make a custom Wizard for Unreal Editor
21 Jun
Written By Roderick Kennedy
I wanted to create a wizard in the trueSKY Unreal plugin that would make it easier for users to add trueSKY to UE scenes. I was following this video where Epic's Michael Noland describes various ways to modify the Editor. So I made a custom Property Editor window with settings to select a sky sequence, create a TrueSkyLight etc.
But it didn't look very friendly. And implementing a wizard-style Apply button just put a button in amongst the other settings - not great. After some searching in the UE codebase, I discovered the SWizard class that Unreal Editor uses for its own wizards. Here's what you do:
1. Create a class derived from SCompoundWidget containing a TSharedPtr<SWizard>. Mine looks like this:
DECLARE_DELEGATE_FourParams( FOnTrueSkySetup, bool, ADirectionalLight* ,bool, UTrueSkySequenceAsset *);
#define S_DECLARE_CHECKBOX(name) \ bool name; \ ECheckBoxState Is##name##Checked() const { return name ? ECheckBoxState::Checked:ECheckBoxState::Unchecked;} \ void On##name##Changed(ECheckBoxState InCheckedState) {name=(InCheckedState==ECheckBoxState::Checked);}
class STrueSkySetupTool : public SCompoundWidget { public: SLATE_BEGIN_ARGS( STrueSkySetupTool ) :_CreateTrueSkyLight(false) ,_DirectionalLight(nullptr) ,_CreateDirectionalLight(nullptr) ,_Sequence(nullptr) {} / A TrueSkyLight actor performs real-time ambient lighting.*/ SLATE_ARGUMENT(bool,CreateTrueSkyLight) / TrueSKY can drive a directional light to provide sunlight and moonlight./ SLATE_ARGUMENT(ADirectionalLight,DirectionalLight) / If there's no directional light in the scene, you can create one with this checkbox.*/ SLATE_ARGUMENT(bool,CreateDirectionalLight) / The TrueSKY Sequence provides the weather state to render./ SLATE_ARGUMENT(UTrueSkySequenceAsset ,Sequence) / Event called when code is successfully added to the project */ SLATE_EVENT( FOnTrueSkySetup, OnTrueSkySetup ) SLATE_END_ARGS() / Constructs this widget with InArgs */ void Construct( const FArguments& InArgs );
/* Handler for when cancel is clicked / void CancelClicked();
/* Returns true if Finish is allowed / bool CanFinish() const;
/* Handler for when finish is clicked / void FinishClicked();
...
S_DECLARE_CHECKBOX(CreateTrueSkyLight) S_DECLARE_CHECKBOX(ShowAllSequences)
void SetupSequenceAssetItems();
void CloseContainingWindow(); private: /* The wizard widget / TSharedPtr MainWizard; FOnTrueSkySetup OnTrueSkySetup; ... };
The SLATE_ARGUMENT macros allow initialization of named parameters in this style:
TSharedRef TrueSkySetupTool = SNew(STrueSkySetupTool).OnTrueSkySetup(OnTrueSkySetup1).CreateTrueSkyLight(true);
etc. This is super-useful.
2. Create a callback for the wizard to execute:
FOnTrueSkySetup OnTrueSkySetupDelegate;
3. Create a window for the widget. This function is called when the menu option to start the wizard is selected:
void FTrueSkyEditorPlugin::OnAddSequence() { TrueSkySetupWindow = SNew(SWindow) .Title( NSLOCTEXT("InitializeTrueSky", "WindowTitle", "Initialize trueSKY") ) .ClientSize( FVector2D(600, 550) ) .SizingRule( ESizingRule::FixedSize ) .SupportsMinimize(false).SupportsMaximize(false); OnTrueSkySetupDelegate.BindRaw(this,&FTrueSkyEditorPlugin::OnTrueSkySetup); TSharedRef TrueSkySetupTool = SNew(STrueSkySetupTool).OnTrueSkySetup(OnTrueSkySetupDelegate); TrueSkySetupWindow->SetContent( TrueSkySetupTool );
If the main frame exists parent the window to it. The main frame should always exist...
TSharedPtr< SWindow > ParentWindow; if( FModuleManager::Get().IsModuleLoaded( "MainFrame" ) ) { IMainFrameModule& MainFrame = FModuleManager::GetModuleChecked( "MainFrame" ); ParentWindow = MainFrame.GetParentWindow(); }
bool modal=false; if (modal) { FSlateApplication::Get().AddModalWindow(TrueSkySetupWindow.ToSharedRef(), ParentWindow); } else if (ParentWindow.IsValid()) { FSlateApplication::Get().AddWindowAsNativeChild(TrueSkySetupWindow.ToSharedRef(), ParentWindow.ToSharedRef()); } else { FSlateApplication::Get().AddWindow(TrueSkySetupWindow.ToSharedRef()); } TrueSkySetupWindow->ShowWindow(); }
4. Implement the setup tool:
BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION void STrueSkySetupTool::Construct( const FArguments& InArgs ) { OnTrueSkySetup = InArgs._OnTrueSkySetup; CreateTrueSkyLight=InArgs._CreateTrueSkyLight; DirectionalLight=InArgs._DirectionalLight; Sequence=InArgs._Sequence; ...
The interface to build the actual UI is really interesting. By overloading the [] and + operators, Epic lets you specify the widget structure like so:
ChildSlot [ SNew(SBorder) .Padding(18) .BorderImage( FEditorStyle::GetBrush("Docking.Tab.ContentAreaBrush") ) [ SNew(SVerticalBox) +SVerticalBox::Slot() [ SAssignNew( MainWizard, SWizard) .ShowPageList(false) .CanFinish(this, &STrueSkySetupTool::CanFinish) .FinishButtonText( LOCTEXT("TrueSkyFinishButtonText", "Initialize") ) .OnCanceled(this, &STrueSkySetupTool::CancelClicked) .OnFinished(this, &STrueSkySetupTool::FinishClicked) .InitialPageIndex( 0) +SWizard::Page() [ SNew(SVerticalBox) +SVerticalBox::Slot() .AutoHeight() [ SNew(STextBlock) .TextStyle( FEditorStyle::Get(), "NewClassDialog.PageTitle" ) .Text( LOCTEXT( "WeatherStateTitle", "Choose a Sequence Asset" ) ) ] +SVerticalBox::Slot() .AutoHeight() .Padding(0) [ SNew(SHorizontalBox) +SHorizontalBox::Slot() .FillWidth(1.f) .VAlign(VAlign_Center) [ SNew(STextBlock) .Text(LOCTEXT("TrueSkySetupToolDesc", "Choose which weather sequence to use initially.") ) .AutoWrapText(true) .TextStyle(FEditorStyle::Get(), "NewClassDialog.ParentClassItemTitle") ] ] ] +SWizard::Page() [ ... ] ] ] ];
}
So by adding new +SWizard::Page() elements we add pages to the wizard.
5. Finally, implement the callback that the delegate calls when you click "Finish":
void FTrueSkyEditorPlugin::OnTrueSkySetup(bool CreateDirectionalLight, ADirectionalLight DirectionalLight,bool CreateTrueSkyLight,UTrueSkySequenceAsset Sequence) { ... }
The end result looks like this:
Full source for this is at our UE branch, (register at Simul to access).