How to make a dockable tab in a plugin. Also Hamad's Plugin corrected - Unreal 4.7.0

This took so much effort and time, I thought I’d leave a trail of breadcrumbs into the deep dark woods.

I am no expert so some of the statements in this may be incorrect, but it DOES work.

The attached example code works in Unreal 4.7.0

What I wanted was a plugin and a tab panel with some buttons and GUI in it to place content rapidly in our world map.

There were two major hurdles.

  1. Getting a Plugin working at all.

  2. Getting a tab panel to pop.

#1 is difficult because the example on these forums is the HamadsPlugin example, and the code that
is downloadable from his web site is broken. You MUST read the forum comments to see the fixes to the code.

  • Getting a plugin to build requires VERY careful name convention following. If you plugin is called MySuperduperPlugin then it must be
    exactly that. The .uplugin, the C++ files for the .h and .cpp MUST be exactly the correct name, and
    references in the MySuperduperPlugin.Build.cs must be exactly correct. The Unreal code generators that build and
    add in the plugin assume the names are exact.

  • You MUST use the menu on mygame.uproject to “Generate Visual Studio project files”.

Ok, good we got Hamad’s plugin to build, but of course I changed it to the name of my plugin in my game.
(Which is where I messed around for hours discovering that the names are super picky.)

#2 Is simple but very difficult to discover. I recommend getting the source to the entire Unreal Engine from git hub as a reference and sample code.

  • In your StartupModule() method you want to register the tab with the tab manager. You do NOT make the tab and force it in place.
    The tab manager handles all that and keeps a copy of the tab pane around for reuse and such.

  • Before you can do that you need a name for you tab, the FTabId. Turns out this is a static FName you declare in the .cpp file of your plugin or any other .cpp file.
    Like this…


static const FName SuperduperPluginTabName("MySuperduperTab");

Now in your code this is the TabID. You do NOT keep some pointer or reference to your tab when you create it.
It gets registered with the TabId and kept in the editor’s tab manager.

  • So now we register the function callback to create the actual tab.
    Inside the StartupModule() somewhere…


TSharedRef<class FGlobalTabmanager> tm = FGlobalTabmanager::Get();
tm->RegisterTabSpawner(SuperduperPluginTabName, FOnSpawnTab::CreateRaw(this, &MySuperduperPlugin::SpawnTab))
		.SetDisplayName(FText::FromString( TEXT("SuperPlugin") ));


Well golly, what is that FOnSpawnTab::CreateRaw(this, &MySuperduperPlugin::SpawnTab) do?

CreateRaw creates a delegate pointer in C++ so external code (the editor) can call a function in this object (the plugin).
So you give it a this pointer and the address of the function &MySuperduperPlugin::SpawnTab

Now when you ask the tab manager to pop open you tab, it will call the this->SpawnTab funtion to create the tab, and if you call it again it reuses the tab.

  • So lets pop it open…


void MySuperduperPlugin::MyButton_Clicked()
{
	TSharedRef<class FGlobalTabmanager> tm = FGlobalTabmanager::Get();
	tm->InvokeTab(SuperduperPluginTabName);
}


  • Well of course we want to create the tab function too so it can be called…

In the .h file…


TSharedRef<SDockTab> SpawnTab(const FSpawnTabArgs& TabSpawnArgs);

And in the plugin .cpp file…




TSharedRef<SDockTab> MySuperduperPlugin::SpawnTab(const FSpawnTabArgs& TabSpawnArgs)
{
	TSharedRef<SDockTab> SpawnedTab = SNew(SDockTab)
		SNew(SButton)
			.Text(FText::FromString(TEXT("Push Me")))
			.ContentPadding(3)
	];

	return SpawnedTab;
}



That code just creates a free floating top level tab window that has a single giant button in the middle that says Push Me on it.

You can dock the tab at the top level of the editor.

  • Some cleanup is in order. In ShutdownModule() you unregister the tab…


		TSharedRef<class FGlobalTabmanager> tm = FGlobalTabmanager::Get();
		tm->UnregisterTabSpawner(SuperduperPluginTabName);


The entire source of the plugin is attached. It will be the basis of a plugin I am writing.
Feel free to use it as a starting point but please do NOT use the “ActorPainter” or derrived names.

Todo: I need to figure out how to allow it to dock in subwindows. If I get that I’ll continue this post.

– Later…

WooHoo! Figured it out.
You instead call **RegisterNomadTabSpawner **and mark the tab as .TabRole(ETabRole::NomadTab) when you create it.

Like…


	
tm->RegisterNomadTabSpawner(ActorPainterTabName, FOnSpawnTab::CreateRaw(this, &ActorPainterPlugin::SpawnTab))
		.SetDisplayName(FText::FromString( TEXT("Actor Painter") ));


and…


	TSharedRef<SDockTab> SpawnedTab = SNew(SDockTab)
		.TabRole(ETabRole::NomadTab)
	
		SNew(SButton)
			.Text(FText::FromString(TEXT("Actor Spawner")))
			.ContentPadding(3)
	];


Remember - The mountain is high but the mule is patient.

There is a FTabManager in the LevelEditorModule (LevelEditorModule.GetLevelEditorTabManager()). This tab manager allows you to dock to level editor docks.
However that tab manager is reset when the content area is restored. You can add a delegate to LevelEditorModule.OnTabManagerChanged() and then always register/unregister your tab spawner when the manager changes.

There may be an easier way :confused:

Addendum - Making checkbox callbacks work in the panel.

Addendum:

I began working toward getting a panel with a checkbox and a button. This highlighted a mistake in the above sample code.

Things like SButton and SCheckbox need callbacks and the callbacks require the creating class to be a Widget derrivative.

So you need a class like SActorPainterWidget, which is…

The .h file.



#pragma once
#include "ActorPainterPluginPCH.h"

#include "ActorPainterPlugin.h"
#include "SDockTab.h"

class SActorPainterWidget : public SCompoundWidget
{
public:

	SLATE_BEGIN_ARGS(SActorPainterWidget) {}

	SLATE_END_ARGS()

public:

	void Construct(const FArguments& InArgs);

	ECheckBoxState GetPaintingActive() const
	{
		return PaintingEnabled ? ECheckBoxState::Checked : ECheckBoxState::Unchecked;
	}

	void OnPaintingCheckboxChanged(ECheckBoxState NewState)
	{
		PaintingEnabled = (NewState == ECheckBoxState::Checked);
	}

private:

	bool PaintingEnabled;

};


The .cpp file



#include "ActorPainterPluginPCH.h"

#include "ActorPainterPlugin.h"
#include "ActorPainterWidget.h"

#define LOCTEXT_NAMESPACE "SActorPainterWidget"

void SActorPainterWidget::Construct(const FArguments& InArgs)
{
	UE_LOG(TDLLog, Log, TEXT("SActorPainterWidget::Construct..."));

	PaintingEnabled = false;

	
		
			SNew(SVerticalBox)
			+ SVerticalBox::Slot()
			.AutoHeight()
			
				SNew(SHorizontalBox)
				+ SHorizontalBox::Slot()
				.AutoWidth()
				
					SNew(SCheckBox)
					.IsChecked(this, &SActorPainterWidget::GetPaintingActive)
					.OnCheckStateChanged(this, &SActorPainterWidget::OnPaintingCheckboxChanged)
					.ToolTipText(FText::FromString(TEXT("Sprays the blueprints when checked and mouse down.")))
				]
			]
			+ SVerticalBox::Slot()
				.AutoHeight()
				
					SNew(SButton)
					.Text(FText::FromString(TEXT("Clear")))
					.ContentPadding(3)
				]
		];
}

#undef LOCTEXT_NAMESPACE


Then the ActorPainterPlugin changes to just make the tab panel and fill it in with the ActorPainterWidget.



TSharedRef<SDockTab> ActorPainterPlugin::SpawnTab(const FSpawnTabArgs& TabSpawnArgs)
{
	TSharedRef<SDockTab> SpawnedTab = SNew(SDockTab)
		.TabRole(ETabRole::NomadTab)
	
		SNew(SActorPainterWidget)
	];

	return SpawnedTab;
}


Which is more modular and separates concerns nicely.

Oh, hey, that thingy in the code.

A Widget sometimes needs to refer to its own contents, for example when constructing its self.

The is that reference.

Unreal have overloaded the operator] of Widgets to make for nifty syntax candy for making Slate Widgets.
Once you get used to it it is quite nice.

Once again, I suggest getting the entire Unreal Engine source from GitHub and peruse it for samples of UI elemts and layout.

More…

I realized that what I really want is a window in the Modes tab in the editor, so my plugin acts just like the Foliage tool.

This led me to figure out the extension architecture of the Level Editor Modes (FEdMode) etc.

The way I did it was to look at the Foliage Edito tool in

\Engine\Source\Editor\FoliageEdit of the GitHub source to the Unreal Engine.

I then adapted the above example code until it worked.

(This also has the code to freeze the editor view camera while you move the mouse and ‘do something’ to the scene with left mouse held down.)

The Level Editor has a plugin style setup where you register your Mode. A Mode is a single tab in the Level Editor area like the Terrain Brush, or the Foliage Editor.
So registration is the same…



	FEditorModeRegistry::Get().RegisterMode<FActorPainterEdMode>(
		SActorPainterWidget::ActorPainterEdModeName,
		FText::FromString( TEXT("Actor Painter") ),
		FSlateIcon(FEditorStyle::GetStyleSetName(), "LevelEditor.FoliageMode", "LevelEditor.FoliageMode.Small"),
		true, 400
		);


The Actor Painter Widget with the brush size slider etc. stays the same.

I removed the code that makes the icon in the center top of the editor, and also removed the code that makes the nomad tab window.
( lol, that was the original purpose and post title, but this is cooler. )

You make a class from FEdMode

FActorPainterEdMode.h



#pragma once

#include "ActorPainterPluginPCH.h"
#include "EdMode.h"

class SActorPainterWidget;

class FActorPainterEdMode : public FEdMode
{
public:

	/** Constructor */
	FActorPainterEdMode();

	/** Destructor */
	virtual ~FActorPainterEdMode();

	/** FGCObject interface */
	virtual void AddReferencedObjects(FReferenceCollector& Collector) override;

	/** FEdMode: Called when the mode is entered */
	virtual void Enter() override;

	/** FEdMode: Called when the mode is exited */
	virtual void Exit() override;

	/** FEdMode: Called after an Undo operation */
	virtual void PostUndo() override;

	/**
	* Called when the mouse is moved over the viewport
	*
	* @param	InViewportClient	Level editor viewport client that captured the mouse input
	* @param	InViewport			Viewport that captured the mouse input
	* @param	InMouseX			New mouse cursor X coordinate
	* @param	InMouseY			New mouse cursor Y coordinate
	*
	* @return	true if input was handled
	*/
	virtual bool MouseMove(FEditorViewportClient* ViewportClient, FViewport* Viewport, int32 x, int32 y) override;
	virtual bool CapturedMouseMove(FEditorViewportClient* ViewportClient, FViewport* Viewport, int32 MouseX, int32 MouseY) override;
	virtual bool InputDelta(FEditorViewportClient* InViewportClient, FViewport* InViewport, FVector& InDrag, FRotator& InRot, FVector& InScale) override;
	virtual void Tick(FEditorViewportClient* ViewportClient, float DeltaTime) override;
	virtual bool InputKey(FEditorViewportClient* ViewportClient, FViewport* Viewport, FKey Key, EInputEvent Event)override;

	void ActorPaintSphereBrushTrace(FEditorViewportClient* ViewportClient, int32 MouseX, int32 MouseY);

	bool bBrushTraceValid;
	FVector BrushLocation;
	FVector LastBrushPaintLocation;
	FVector BrushTraceDirection;
	bool bViewCameraIsLocked;
	FVector LockedCameraLocation;
	FRotator  LockedCameraRotation;
	UStaticMeshComponent* SphereBrushComponent;
	bool bToolActive;

};


And the .cpp




#include "ActorPainterPluginPCH.h"
#include "UnrealEd.h"
#include "ToolkitManager.h"

#include "FActorPainterEdMode.h"
#include "ActorPainterWidget.h"
#include "ActorPainterEdModeToolkit.h"

/** Must have "Landscape" in ActorPainterPlugin.Build.cs */
#include "Landscape.h"

FActorPainterEdMode::FActorPainterEdMode()
	: FEdMode()
	, bToolActive(false)
	, bViewCameraIsLocked(false)
{
	// Load resources and construct brush component
	UMaterial* BrushMaterial = nullptr;
	UStaticMesh* StaticMesh = nullptr;
	if (!IsRunningCommandlet())
	{
		BrushMaterial = LoadObject<UMaterial>(nullptr, TEXT("/Engine/EditorLandscapeResources/FoliageBrushSphereMaterial.FoliageBrushSphereMaterial"), nullptr, LOAD_None, nullptr);
		StaticMesh = LoadObject<UStaticMesh>(nullptr, TEXT("/Engine/EngineMeshes/Sphere.Sphere"), nullptr, LOAD_None, nullptr);
	}

	SphereBrushComponent = ConstructObject<UStaticMeshComponent>(UStaticMeshComponent::StaticClass());
	SphereBrushComponent->SetCollisionProfileName(UCollisionProfile::NoCollision_ProfileName);
	SphereBrushComponent->SetCollisionObjectType(ECC_WorldDynamic);
	SphereBrushComponent->StaticMesh = StaticMesh;
	SphereBrushComponent->OverrideMaterials.Add(BrushMaterial);
	SphereBrushComponent->SetAbsolute(true, true, true);
	SphereBrushComponent->CastShadow = false;

	bBrushTraceValid = false;
	BrushLocation = FVector::ZeroVector;
	LastBrushPaintLocation = FVector::ZeroVector;
}


/** Destructor */
FActorPainterEdMode::~FActorPainterEdMode()
{
	FEditorDelegates::MapChange.RemoveAll(this);
}

void FActorPainterEdMode::AddReferencedObjects(FReferenceCollector& Collector)
{
	// Call parent implementation
	FEdMode::AddReferencedObjects(Collector);

	Collector.AddReferencedObject(SphereBrushComponent);
}

void FActorPainterEdMode::Enter()
{
	FEdMode::Enter();

	UE_LOG(TDLLog, Log, TEXT("FActorPainterEdMode::Enter..."));

	if (!Toolkit.IsValid())
	{
		Toolkit = MakeShareable(new FActorPainterEdModeToolkit);
		Toolkit->Init(Owner->GetToolkitHost());
	}

}

/** FEdMode: Called when the mode is exited */
void FActorPainterEdMode::Exit()
{
	UE_LOG(TDLLog, Log, TEXT("FActorPainterEdMode::Exit..."));

	FToolkitManager::Get().CloseToolkit(Toolkit.ToSharedRef());
	Toolkit.Reset();

	// Turn off the sphere.
	if (SphereBrushComponent->IsRegistered())
	{
		SphereBrushComponent->UnregisterComponent();
	}

	bViewCameraIsLocked = false;

	// Call base Exit method to ensure proper cleanup
	FEdMode::Exit();
}

void FActorPainterEdMode::PostUndo()
{
	FEdMode::PostUndo();
	UE_LOG(TDLLog, Log, TEXT("FActorPainterEdMode::PostUndo..."));
}

bool FActorPainterEdMode::MouseMove(FEditorViewportClient* ViewportClient, FViewport* Viewport, int32 x, int32 y)
{
	//UE_LOG(TDLLog, Log, TEXT("FActorPainterEdMode::MouseMove %d %d..."), x, y);
	ActorPaintSphereBrushTrace(ViewportClient, x, y);

	if (bBrushTraceValid && SActorPainterWidget::GlobalActorPainterWidget != NULL && FVector::DistSquared(LastBrushPaintLocation, BrushLocation) > SActorPainterWidget::GlobalActorPainterWidget->BrushSizeMeters * 0.1f)
	{
		LastBrushPaintLocation = BrushLocation;
	}

	return false;
}

bool FActorPainterEdMode::CapturedMouseMove(FEditorViewportClient* ViewportClient, FViewport* Viewport, int32 x, int32 y)
{
	//UE_LOG(TDLLog, Log, TEXT("FActorPainterEdMode::CapturedMouseMove %d %d..."), x, y);
	ActorPaintSphereBrushTrace(ViewportClient, x, y);

	if (bBrushTraceValid && SActorPainterWidget::GlobalActorPainterWidget != NULL && FVector::DistSquared(LastBrushPaintLocation, BrushLocation) > SActorPainterWidget::GlobalActorPainterWidget->BrushSizeMeters * 0.1f)
	{
		LastBrushPaintLocation = BrushLocation;
	}

	return false;
}

bool FActorPainterEdMode::InputDelta(FEditorViewportClient* InViewportClient, FViewport* InViewport, FVector& InDrag, FRotator& InRot, FVector& InScale)
{
	//UE_LOG(TDLLog, Log, TEXT("FActorPainterEdMode::InputDelta..."));
	return false;
}

void FActorPainterEdMode::ActorPaintSphereBrushTrace(FEditorViewportClient* ViewportClient, int32 MouseX, int32 MouseY)
{
	bBrushTraceValid = false;
	if (!ViewportClient->IsMovingCamera() || bViewCameraIsLocked)
	{
		// Compute a world space  from the screen space mouse coordinates
		FSceneViewFamilyContext ViewFamily(FSceneViewFamily::ConstructionValues(
			ViewportClient->Viewport,
			ViewportClient->GetScene(),
			ViewportClient->EngineShowFlags)
			.SetRealtimeUpdate(ViewportClient->IsRealtime()));
		FSceneView* View = ViewportClient->CalcSceneView(&ViewFamily);
		FViewportCursorLocation MouseViewportRay(View, ViewportClient, MouseX, MouseY);

		FVector Start = MouseViewportRay.GetOrigin();
		BrushTraceDirection = MouseViewportRay.GetDirection();
		FVector End = Start + WORLD_MAX * BrushTraceDirection;

		TArray<FHitResult> Hits;
		UWorld* World = ViewportClient->GetWorld();

		FCollisionQueryParams Params;
		FCollisionObjectQueryParams ObjectQueryParams;
		bool hasHits = World->LineTraceMulti( Hits, Start, End, Params, ObjectQueryParams);

		if (hasHits)
		{
			for (int i = 0; i < Hits.Num(); i++)
			{
				FHitResult hr = Hits*;
				AActor * act = hr.Actor.Get();
				if (act->IsA(ALandscape::StaticClass()))
				{
					BrushLocation = hr.Location;
					bBrushTraceValid = true;
					//UE_LOG(TDLLog, Log, TEXT("FActorPainterEdMode::ActorPaintSphereBrushTrace %f %f %f %s"), BrushLocation.X, BrushLocation.Y, BrushLocation.Z, *(act->GetName()));
					break;
				}
			}
		}
	}
}

void FActorPainterEdMode::Tick(FEditorViewportClient* ViewportClient, float DeltaTime)
{
	FEdMode::Tick(ViewportClient, DeltaTime);

	if (bViewCameraIsLocked)
	{
		ViewportClient->SetViewLocation( LockedCameraLocation );
		ViewportClient->SetViewRotation( LockedCameraRotation );
	}

	// Update the position and size of the brush component
	if (bBrushTraceValid && SActorPainterWidget::GlobalActorPainterWidget != NULL)
	{
		if (!SphereBrushComponent->IsRegistered())
		{
			//UE_LOG(TDLLog, Log, TEXT("FActorPainterEdMode::Tick Reg..."));
			SphereBrushComponent->RegisterComponentWithWorld(ViewportClient->GetWorld());
		}

		// Scale adjustment is due to default sphere SM size.
		FTransform BrushTransform = FTransform(FQuat::Identity, BrushLocation, FVector(SActorPainterWidget::GlobalActorPainterWidget->BrushSizeMeters * 0.00625f * 50.0f));
		SphereBrushComponent->SetRelativeTransform(BrushTransform);
	}
	else
	{
		if (SphereBrushComponent->IsRegistered())
		{
			//UE_LOG(TDLLog, Log, TEXT("FActorPainterEdMode::Tick Unreg..."));
			SphereBrushComponent->UnregisterComponent();
		}
	}
}

bool FActorPainterEdMode::InputKey(FEditorViewportClient* ViewportClient, FViewport* Viewport, FKey Key, EInputEvent Event)
{
	UE_LOG(TDLLog, Log, TEXT("FActorPainterEdMode::InputKey %s"), *Key.GetFName().ToString());

	if (Key == EKeys::LeftMouseButton)
	{
		if (Event == EInputEvent::IE_Pressed)
		{
			bViewCameraIsLocked = true;
			LockedCameraLocation = ViewportClient->GetViewLocation();
			LockedCameraRotation = ViewportClient->GetViewRotation();
		}
		else if (Event == EInputEvent::IE_Released)
		{
			bViewCameraIsLocked = false;
		}
	}

	return false;
}



And you need a toolkit which is what creates the Widget so it gets put in as a tab under the Modes window.



// Copyright 1998-2015 Epic Games, Inc. All Rights Reserved.

#pragma once

#include "ActorPainterPluginPCH.h"
#include "Toolkits/BaseToolkit.h"

/**
 * Public interface to ActorPainter Edit mode.
 */
class FActorPainterEdModeToolkit : public FModeToolkit
{
public:
	virtual void RegisterTabSpawners(const TSharedRef<class FTabManager>& TabManager) override;
	virtual void UnregisterTabSpawners(const TSharedRef<class FTabManager>& TabManager) override;

	/** Initializes the ActorPainter mode toolkit */
	virtual void Init(const TSharedPtr< class IToolkitHost >& InitToolkitHost);

	/** IToolkit interface */
	virtual FName GetToolkitFName() const override;
	virtual FText GetBaseToolkitName() const override;
	virtual class FEdMode* GetEditorMode() const override;
	virtual TSharedPtr<class SWidget> GetInlineContent() const override;

	void PostUndo();

private:
	TSharedPtr< class SActorPainterWidget > ActorPainterEdWidget;
};


and the .cpp



// Copyright 1998-2015 Epic Games, Inc. All Rights Reserved.

#include "ActorPainterPluginPCH.h"
#include "UnrealEd.h"
#include "ActorPainterEdModeToolkit.h"

#include "ActorPainterWidget.h"

#define LOCTEXT_NAMESPACE "ActorPainterEditMode"

void FActorPainterEdModeToolkit::RegisterTabSpawners(const TSharedRef<class FTabManager>& TabManager)
{

}

void FActorPainterEdModeToolkit::UnregisterTabSpawners(const TSharedRef<class FTabManager>& TabManager)
{

}

void FActorPainterEdModeToolkit::Init(const TSharedPtr< class IToolkitHost >& InitToolkitHost)
{
	ActorPainterEdWidget = SNew(SActorPainterWidget);

	FModeToolkit::Init(InitToolkitHost);
}

FName FActorPainterEdModeToolkit::GetToolkitFName() const
{
	return FName("ActorPainterEditMode");
}

FText FActorPainterEdModeToolkit::GetBaseToolkitName() const
{
	return FText::FromString( TEXT( "Actor Painter Edit Mode" ));
}

class FEdMode* FActorPainterEdModeToolkit::GetEditorMode() const
{
	return GLevelEditorModeTools().GetActiveMode(SActorPainterWidget::ActorPainterEdModeName);
}

TSharedPtr<SWidget> FActorPainterEdModeToolkit::GetInlineContent() const
{
	return ActorPainterEdWidget;
}

void FActorPainterEdModeToolkit::PostUndo()
{
	// When an undo relates to the ActorPainter Edit mode, refresh the list.
	//ActorPainterEdWidget->RefreshFullList();
}

#undef LOCTEXT_NAMESPACE



ty for taking the time to document all this!!

Thanks for putting all this great info up! Very helpful

One question though, your example only seems to Dock with the Main/Global tab of the Unreal Editor. Is it possible to make it dockable within the tabs in the editor (eg: Properties, ContentBrowser etc.)

I had a look through the Widget Reflector but I’m a bit unclear how to register my plugin slate window with the TabManager/SDockingTabStack

Thank you very much for doing this tutorial It’s really helpful!

Thank you thank you thank you thank you!
Been rooting around in the engine code for 2 days trying to do this!