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<SWizard> MainWizard;
FOnTrueSkySetup OnTrueSkySetup;
...
};
The SLATE_ARGUMENT macros allow initialization of named parameters in this style:
TSharedRef<STrueSkySetupTool> 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<imainframemodule>( "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).