Thursday, 21 June 2018

How to make a custom Wizard for Unreal Editor

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
 SLATE_BEGIN_ARGS( STrueSkySetupTool )
 /** A TrueSkyLight actor performs real-time ambient lighting.*/
 /** TrueSKY can drive a directional light to provide sunlight and moonlight.*/
 /** If there's no directional light in the scene, you can create one with this checkbox.*/
 /** 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 )
 /** 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();


 void SetupSequenceAssetItems();
 void CloseContainingWindow();
 /** 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 )
 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());
4. Implement the setup tool:
void STrueSkySetupTool::Construct( const FArguments& InArgs )
 OnTrueSkySetup = InArgs._OnTrueSkySetup;

The interface to build the actual UI is really interesting. By overloading the [] and + operators, Epic lets you specify the widget structure like so:

  .BorderImage( FEditorStyle::GetBrush("Docking.Tab.ContentAreaBrush") )
    SAssignNew( MainWizard, SWizard)
    .CanFinish(this, &STrueSkySetupTool::CanFinish)
    .FinishButtonText(  LOCTEXT("TrueSkyFinishButtonText", "Initialize") )
    .OnCanceled(this, &STrueSkySetupTool::CancelClicked)
    .OnFinished(this, &STrueSkySetupTool::FinishClicked)
    .InitialPageIndex( 0)
      .TextStyle( FEditorStyle::Get(), "NewClassDialog.PageTitle" )
      .Text( LOCTEXT( "WeatherStateTitle", "Choose a Sequence Asset" ) )
       .Text(LOCTEXT("TrueSkySetupToolDesc", "Choose which weather sequence to use initially.") )
       .TextStyle(FEditorStyle::Get(), "NewClassDialog.ParentClassItemTitle")


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).