Slate Tabs

Hey everyone! I’ve been banging my head against a problem for a few days now and was hoping someone could give me a hand. I’m pretty new to Slate and I think I’m misunderstanding things.

I’m trying to add tabs to another tab in a plugin. Registering and invoking tabs is not to issue – adding a TabStack to my primary ArtTools tab and then adding tabs to that TabStack is the issue. In order to do this, I’m particularly looking at SDockTabStack and SDockableTab, but the complete lack of documentation regarding this area of Slate is really tripping me up. Much of the UE4 engine code uses SDockTab and SDockingTabStack as well, somaybe I should just use those, but they don’t seem to be exposed to Plugins (though maybe I’m wrong?).

I’ve scoured AnswerHub and the Google for anything related to this, but none of it has been of much use. Any help would be appreciated!

Hi TBWright,
I was looking into the exact same problem last week and i was able to create several tabs and a tab container from a plugin by following the example of the BlueprintDebugger

In the file BlueprintDebugger.cpp, look into the function
FBlueprintDebuggerImpl::CreateBluprintDebuggerTab

You will need to reproduce most of the code and pattern there

  1. it creates the tab that will be returned, this THE container tab spawn from the plugin (line 101)
  2. you need to create a TabManager that persists the layout configuration and save it from sessions to sessions (line 107 - line 144)
  3. register your individual tabs to THAT TabManager (instead of the global one) line 149 to 167. in this example 3 tabs are registered
  4. describe the default layout and give it a name (line 169)
  5. line 191 to 323 is needed to provide contextual buttons to be able to recreate the tabs if they have been hidden…

It works for my case, i used that exact same code pattern.
i can rearrange the tabs within the plugin tab container as i want and the layout persists from sessions to sessions so that is good enough for me.
one issue is that if you put one of the individual tabs OUTSIDE of that plugin tab it will be hidden on the next session (and you can make it reappear with the context buttons on the plugin main tab)

Hope that help,
The code as is (september 14th 2021)

line 99 : TSharedRef<SDockTab> FBlueprintDebuggerImpl::CreateBluprintDebuggerTab(const FSpawnTabArgs& Args)
{
	const TSharedRef<SDockTab> NomadTab = SNew(SDockTab)
		.TabRole(ETabRole::NomadTab)
		.Label(NSLOCTEXT("BlueprintDebugger", "TabTitle", "Blueprint Debugger"));

	if (!DebuggingToolsTabManager.IsValid())
	{
		DebuggingToolsTabManager = FGlobalTabmanager::Get()->NewTabManager(NomadTab);
		// on persist layout will handle saving layout if the editor is shut down:
		DebuggingToolsTabManager->SetOnPersistLayout(
			FTabManager::FOnPersistLayout::CreateStatic(
				[](const TSharedRef<FTabManager::FLayout>& InLayout)
				{
					if (InLayout->GetPrimaryArea().Pin().IsValid())
					{
						FLayoutSaveRestore::SaveToConfig(GEditorLayoutIni, InLayout);
					}
				}
			)
		);
	}
	else
	{
		ensure(BlueprintDebuggerLayout.IsValid());
	}

	const FName ExecutionFlowTabName = FName(TEXT("ExecutionFlowApp"));
	const FName CallStackTabName = CallStackViewer::GetTabName();
	const FName WatchViewerTabName = WatchViewer::GetTabName();

	TWeakPtr<FTabManager> DebuggingToolsTabManagerWeak = DebuggingToolsTabManager;
	// On tab close will save the layout if the debugging window itself is closed,
	// this handler also cleans up any floating debugging controls. If we don't close
	// all areas we need to add some logic to the tab manager to reuse existing tabs:
	NomadTab->SetOnTabClosed(SDockTab::FOnTabClosedCallback::CreateStatic(
		[](TSharedRef<SDockTab> Self, TWeakPtr<FTabManager> TabManager)
		{
			TSharedPtr<FTabManager> OwningTabManager = TabManager.Pin();
			if (OwningTabManager.IsValid())
			{
				FLayoutSaveRestore::SaveToConfig(GEditorLayoutIni, OwningTabManager->PersistLayout());
				OwningTabManager->CloseAllAreas();
			}
		}
		, DebuggingToolsTabManagerWeak
	));

	if (!BlueprintDebuggerLayout.IsValid())
	{
		DebuggingToolsTabManager->RegisterTabSpawner(
			ExecutionFlowTabName,
			FOnSpawnTab::CreateStatic(
				[](const FSpawnTabArgs&)->TSharedRef<SDockTab>
				{
				return SNew(SDockTab)
					.TabRole(ETabRole::PanelTab)
					.Label(NSLOCTEXT("BlueprintExecutionFlow", "TabTitle", "Execution Flow"))
					[
						SNew(SKismetDebuggingView)
					];
				}
			)
		)
		.SetDisplayName(NSLOCTEXT("BlueprintDebugger", "ExecutionFlowTabTitle", "Blueprint Execution Flow"))
		.SetTooltipText(NSLOCTEXT("BlueprintDebugger", "ExecutionFlowTooltipText", "Open the Blueprint Execution Flow tab."));

		CallStackViewer::RegisterTabSpawner(*DebuggingToolsTabManager);
		WatchViewer::RegisterTabSpawner(*DebuggingToolsTabManager);

		BlueprintDebuggerLayout = FTabManager::NewLayout("Standalone_BlueprintDebugger_Layout_v1")
		->AddArea
		(
			FTabManager::NewPrimaryArea()
			->SetOrientation(Orient_Vertical)
			->Split
			(
				FTabManager::NewStack()
				->SetSizeCoefficient(.4f)
				->SetHideTabWell(true)
				->AddTab(CallStackTabName, ETabState::OpenedTab)
				->AddTab(WatchViewerTabName, ETabState::OpenedTab)
				->AddTab(ExecutionFlowTabName, ETabState::OpenedTab)
				->SetForegroundTab(CallStackTabName)
			)
		);
	}

	BlueprintDebuggerLayout = FLayoutSaveRestore::LoadFromConfig(GEditorLayoutIni, BlueprintDebuggerLayout.ToSharedRef());

	TSharedRef<SWidget> TabContents = DebuggingToolsTabManager->RestoreFrom(BlueprintDebuggerLayout.ToSharedRef(), TSharedPtr<SWindow>()).ToSharedRef();

	// build command list for tab restoration menu:
	TSharedPtr<FUICommandList> CommandList = MakeShareable(new FUICommandList());

	TWeakPtr<FTabManager> DebuggingToolsManagerWeak = DebuggingToolsTabManager;

	const auto ToggleTabVisibility = [](TWeakPtr<FTabManager> InDebuggingToolsManagerWeak, FName InTabName)
	{
		TSharedPtr<FTabManager> InDebuggingToolsManager = InDebuggingToolsManagerWeak.Pin();
		if (InDebuggingToolsManager.IsValid())
		{
			TSharedPtr<SDockTab> ExistingTab = InDebuggingToolsManager->FindExistingLiveTab(InTabName);
			if (ExistingTab.IsValid())
			{
				ExistingTab->RequestCloseTab();
			}
			else
			{
				InDebuggingToolsManager->TryInvokeTab(InTabName);
			}
		}
	};

	const auto IsTabVisible = [](TWeakPtr<FTabManager> InDebuggingToolsManagerWeak, FName InTabName)
	{
		TSharedPtr<FTabManager> InDebuggingToolsManager = InDebuggingToolsManagerWeak.Pin();
		if (InDebuggingToolsManager.IsValid())
		{
			return InDebuggingToolsManager->FindExistingLiveTab(InTabName).IsValid();
		}
		return false;
	};

	CommandList->MapAction(
		FBlueprintDebuggerCommands::Get().ShowCallStackViewer,
		FExecuteAction::CreateStatic(
			ToggleTabVisibility,
			DebuggingToolsManagerWeak,
			CallStackTabName
		),
		FCanExecuteAction::CreateStatic(
			[]() { return true; }
		),
		FIsActionChecked::CreateStatic(
			IsTabVisible,
			DebuggingToolsManagerWeak,
			CallStackTabName
		)
	);

	CommandList->MapAction(
		FBlueprintDebuggerCommands::Get().ShowWatchViewer,
		FExecuteAction::CreateStatic(
			ToggleTabVisibility,
			DebuggingToolsManagerWeak,
			WatchViewerTabName
		),
		FCanExecuteAction::CreateStatic(
			[]() { return true; }
		),
		FIsActionChecked::CreateStatic(
			IsTabVisible,
			DebuggingToolsManagerWeak,
			WatchViewerTabName
		)
	);

	CommandList->MapAction(
		FBlueprintDebuggerCommands::Get().ShowExecutionTrace,
		FExecuteAction::CreateStatic(
			ToggleTabVisibility,
			DebuggingToolsManagerWeak,
			ExecutionFlowTabName
		),
		FCanExecuteAction::CreateStatic(
			[]() { return true; }
		),
		FIsActionChecked::CreateStatic(
			IsTabVisible,
			DebuggingToolsManagerWeak,
			ExecutionFlowTabName
		)
	);

	TWeakPtr<SWidget> OwningWidgetWeak = NomadTab;
	TabContents->SetOnMouseButtonUp(
		FPointerEventHandler::CreateStatic(
			[]( /** The geometry of the widget*/
				const FGeometry&,
				/** The Mouse Event that we are processing */
				const FPointerEvent& PointerEvent,
				TWeakPtr<SWidget> InOwnerWeak,
				TSharedPtr<FUICommandList> InCommandList) -> FReply
			{
				if (PointerEvent.GetEffectingButton() == EKeys::RightMouseButton)
				{
					// if the tab manager is still available then make a context window that allows users to
					// show and hide tabs:
					TSharedPtr<SWidget> InOwner = InOwnerWeak.Pin();
					if (InOwner.IsValid())
					{
						FMenuBuilder MenuBuilder(true, InCommandList);

						MenuBuilder.PushCommandList(InCommandList.ToSharedRef());
						{
							MenuBuilder.AddMenuEntry(FBlueprintDebuggerCommands::Get().ShowCallStackViewer);
							MenuBuilder.AddMenuEntry(FBlueprintDebuggerCommands::Get().ShowWatchViewer);
							MenuBuilder.AddMenuEntry(FBlueprintDebuggerCommands::Get().ShowExecutionTrace);
						}
						MenuBuilder.PopCommandList();


						FWidgetPath WidgetPath = PointerEvent.GetEventPath() != nullptr ? *PointerEvent.GetEventPath() : FWidgetPath();
						FSlateApplication::Get().PushMenu(InOwner.ToSharedRef(), WidgetPath, MenuBuilder.MakeWidget(), PointerEvent.GetScreenSpacePosition(), FPopupTransitionEffect(FPopupTransitionEffect::ContextMenu));

						return FReply::Handled();
					}
				}
				
				return FReply::Unhandled();
			}
			, OwningWidgetWeak
			, CommandList
		)
	);

	NomadTab->SetContent(
		SNew(SBorder)
		.BorderImage(FEditorStyle::GetBrush("ToolPanel.DarkGroupBorder"))
		.Padding(FMargin(0.f, 2.f))
		[
			TabContents
		]
	);

	return NomadTab;
}