Slate UI: Dynamic Menu?

I am trying to see if its possible to dynamically create a menu based on a list of actions that can be performed on a usable actor. For example, I have a usable actor with 2 actions that are “Use” and “Open”. I am trying to make it when you hold down the Use Key for so long it opens up a context menu with a list of available actions. I have it working to call the function to show and hide the menu, however I can’t get the menu list to update so it currently is an empty menu.

Here is my code for my Slate Widget as it currently stands.

SUIInteractionText.h



#pragma once

/**
*
*/
class EXILE_API SUIInteractionText : public SCompoundWidget
{
protected:
	FText GetTextValue() const;

	UFont* Font;

	bool bUseMenuOpen;

public:
	SLATE_BEGIN_ARGS(SUIInteractionText)
	{
	}
	SLATE_ARGUMENT(TWeakObjectPtr<class AExileHUD>, OwnerHUD)
	SLATE_END_ARGS()

	/** Constructs this widget with InArgs */
	void Construct(const FArguments& InArgs);

	void ToggleUseMenu(bool bOpen);

private:
	TWeakObjectPtr<class AExileHUD> OwnerHUD;

	TSharedRef<SWidget> GetUseMenu();
	TSharedPtr<SWidget> UseMenu;

	//Start  button callback 
	FReply UseMenuButtonClick();

};


SUIInteractionText.cpp



#include "SUIInteractionText.h"

BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION
void SUIInteractionText::Construct(const FArguments& InArgs)
{
	OwnerHUD = InArgs._OwnerHUD;

	bUseMenuOpen = false;

	UseMenu = GetUseMenu();
	//UseMenu.Get().

	if (GEngine)
	{
		FString Msg = "Num Children: ";
		Msg.AppendInt(UseMenu.Get()->GetChildren()->Num());
		GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Blue, Msg);
	}

	ChildSlot
	.VAlign(VAlign_Fill)
	.HAlign(HAlign_Fill)
	
		SNew(SOverlay)
		+ SOverlay::Slot()
		.VAlign(VAlign_Center)
		.HAlign(HAlign_Center)
		.Padding(FMargin(0, 0, 0, 40))
		
			SNew(STextBlock)
			.ShadowColorAndOpacity(FLinearColor::Black)
			.ColorAndOpacity(FLinearColor::White)
			.ShadowOffset(FIntPoint(-1, 1))
			.Font(FSlateFontInfo("RobotoDistanceField", 12))
			.Text(this, &SUIInteractionText::GetTextValue)
		]
		+ SOverlay::Slot()
		.HAlign(HAlign_Center)
		.VAlign(VAlign_Center)
		
			UseMenu.ToSharedRef()
		]
	];
}

FText SUIInteractionText::GetTextValue() const
{
	FString String = "";

	if (bUseMenuOpen)
	{
		return FText::FromString(String);
	}

	if (OwnerHUD.IsValid() && OwnerHUD->PlayerOwner->IsValidLowLevel() && OwnerHUD->PlayerOwner->AcknowledgedPawn->IsValidLowLevel())
	{
		AExileCharacter* P = Cast<AExileCharacter>(OwnerHUD->PlayerOwner->AcknowledgedPawn);
		if (P && P->FocusedUsableActor)
		{
			TArray<FName> Actions = P->FocusedUsableActor->GetActionList();

			if (Actions.Num() > 0)
			{
				FName Action = Actions[0];

				String = "Press [USE] to ";

				String.Append(Action.ToString());
			}

		}
	}
	return FText::FromString(String);
}

void SUIInteractionText::ToggleUseMenu(bool bOpen)
{
	bUseMenuOpen = bOpen;

	UseMenu = GetUseMenu();

	if (GEngine)
	{
		FString Msg = "Num Children: ";
		Msg.AppendInt(UseMenu.Get()->GetChildren()->Num());
		GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Blue, Msg);
	}

	
}

TSharedRef<SWidget> SUIInteractionText::GetUseMenu()
{
	FMenuBuilder MenuBuilder(true, NULL, TSharedPtr<FExtender>(), false, &FCoreStyle::Get());
	{

		if (OwnerHUD.IsValid() && OwnerHUD->PlayerOwner->IsValidLowLevel() && OwnerHUD->PlayerOwner->AcknowledgedPawn->IsValidLowLevel())
		{
			AExileCharacter* P = Cast<AExileCharacter>(OwnerHUD->PlayerOwner->AcknowledgedPawn);
			if (P && P->FocusedUsableActor)
			{
				TArray<FName> Actions = P->FocusedUsableActor->GetActionList();

				if (GEngine)
				{
					FString Msg = "Num Actions: ";
					Msg.AppendInt(Actions.Num());
					GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Blue, Msg);
				}

				for (int32 i = 0; i < Actions.Num(); i++)
				{
					MenuBuilder.AddWidget(SNew(SButton)
						.Text(FText::FromString(Actions*.ToString()))
						.OnClicked(this, &SUIInteractionText::UseMenuButtonClick)
						, FText::GetEmpty());
				}
			}
		}
	}


	return  MenuBuilder.MakeWidget();
}

FReply SUIInteractionText::UseMenuButtonClick()
{
	return FReply::Handled();
}

END_SLATE_FUNCTION_BUILD_OPTIMIZATION



I know it is probably something simple, but I just can’t seem to think of the solution. I am so close to getting this functionality to work but still new to Slate so I am still figuring out how to do dynamic stuff in Slate.

The problem is here:


void SUIInteractionText::ToggleUseMenu(bool bOpen)
{
	bUseMenuOpen = bOpen;

	**UseMenu = GetUseMenu();**
        ........


you’re assigning pointer value but it doesnt affect your Overlay slot with that widget. So to make it work you should store referecne to SOverlay you create in Construct function and then use AddSlot/RemoveSlot function whenever you want to “ToggleUseMenu”. However, this will be really inconvenient to manage and keep track of your slots. I would recommend doing it using FSlateApplication::Get().PushMenu function, heres a very simple example that should put you on a right track:



void CreateContexMenu()
	{
		const bool CloseAfterSelection = true;

		// Create menu builder. Note that you dont have to pass optional parameters!
		FMenuBuilder MenuBuilder(CloseAfterSelection, NULL);

		// Create a struct with static function that will be called when one of the entries from the menu is clicked
		struct Local
		{
			/*
			* This function will be called as "OnClick" function for our entries
			* @param ActionIndex - Index of an action that was clicked
			* NOTE: This function ideally would be part of your class. I made it struct/static just for the example!
			*/
			static void OnActionActivated(int32 ActionIndex)
			{

			}
		};

		// Begin menu section
		MenuBuilder.BeginSection("TestSection");
		{
			// Create Action container that will be used for callbacks
			FUIAction Action;

			// Get random number of entries to demonstrate dynamic number of entries
			FRandomStream rand;
			rand.GenerateNewSeed();
			int32 EntriesNum = rand.RandRange(1, 5);

			// 1st entry should be a label, name or whatever
			MenuBuilder.AddWidget(
				SNew(STextBlock)
				.Text(FText::FromString("Label here...")),
				FText(),
				true);

			for (int32 i = 0; i < EntriesNum; i++)
			{
				// For each entry create new action that will pass coresponding index
				Action = FUIAction(FExecuteAction::CreateStatic(&Local::OnActionActivated, i));

				// Finally, add menu entry.
				// Note that you can always use AddWidget(SNew(SButton)...) here but if entry is supposed to act like a buttuon only, AddMenuEntry should do the trick
				MenuBuilder.AddMenuEntry(
					FText::FromString(FString::Printf(TEXT("Entry no. %d"), i)),
					FText(),
					FSlateIcon(), Action);
			}
			
		}
		MenuBuilder.EndSection();


		// Pop up menu at cursor position with some default transition effect
		FSlateApplication::Get().PushMenu(SharedThis(this), MenuBuilder.MakeWidget(), FSlateApplication::Get().GetCursorPos(), FPopupTransitionEffect(FPopupTransitionEffect::ContextMenu));
	}


If you will have any problems with understanding this example, let me know. Hope this helps.

Thanks szyszek,

After posting this I have tried using the PushMenu and it seems to work great. However, it seems to pause the entire game when the menu is up. I would like it to not pause the game and have the menu also show up directly in the center of the screen and not the cursor pointer location. Also it doesn’t seem to close the menu when I click one of the buttons. I am still using the code I wrote for the menu builder which has the menu items as SButtons. As for the problem in my original code, I saw that and couldn’t figure out how to go about adding slots to a vertical box dynamically and clearing them out on close. Creating static content in slate and dynamic changing text widget properties are pretty easy but when it comes to adding/removing slots at run time is a real pain or I am just overlooking something.

Thanks again for your help, it pushed me in the right direction and now just need to figure out how to get it to work correctly.

I have solved the problem :D. Now to finish up the code and get it to fully function like I want. Thanks for the help in pointing me in the correct direction.

Here is the updated code if anyone is interested in it or runs into a similar problem.

SUIInteractionText.h



#pragma once

/**
*
*/
class EXILE_API SUIInteractionText : public SCompoundWidget
{
protected:
	FText GetTextValue() const;

	bool bUseMenuOpen;

public:
	SLATE_BEGIN_ARGS(SUIInteractionText)
	{
	}
	SLATE_ARGUMENT(TWeakObjectPtr<class AExileHUD>, OwnerHUD)
	SLATE_END_ARGS()

	/** Constructs this widget with InArgs */
	void Construct(const FArguments& InArgs);

	void ToggleUseMenu(bool bOpen);

private:
	TWeakObjectPtr<class AExileHUD> OwnerHUD;

	TSharedRef<SWidget> GetUseMenu();
	SOverlay::FOverlaySlot* HelpTextSlot;
	SOverlay::FOverlaySlot* UseMenuSlot;

	//Start  button callback 
	FReply UseMenuButtonClick(FName Action);

};


SUIInteractionText.cpp



#include "Exile.h"
#include "SUIInteractionText.h"

BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION
void SUIInteractionText::Construct(const FArguments& InArgs)
{
	OwnerHUD = InArgs._OwnerHUD;

	bUseMenuOpen = false;

	ChildSlot
	.VAlign(VAlign_Fill)
	.HAlign(HAlign_Fill)
	
		SNew(SOverlay)
		+ SOverlay::Slot()
		.VAlign(VAlign_Center)
		.HAlign(HAlign_Center)
		.Expose(HelpTextSlot)
		.Padding(FMargin(0, 0, 0, 40))
		
			SNew(STextBlock)
			.ShadowColorAndOpacity(FLinearColor::Black)
			.ColorAndOpacity(FLinearColor::White)
			.ShadowOffset(FIntPoint(-1, 1))
			.Font(FSlateFontInfo("RobotoDistanceField", 12))
			.Text(this, &SUIInteractionText::GetTextValue)
		]
		+ SOverlay::Slot()
		.HAlign(HAlign_Center)
		.VAlign(VAlign_Center)
		.Expose(UseMenuSlot) // Expose it to a pointer so we can change the widget at runtime
		
			SNullWidget::NullWidget
		]
	];
}

FText SUIInteractionText::GetTextValue() const
{
	FString String = "";

	if (bUseMenuOpen)
	{
		return FText::FromString(String);
	}

	if (OwnerHUD.IsValid() && OwnerHUD->PlayerOwner->IsValidLowLevel() && OwnerHUD->PlayerOwner->AcknowledgedPawn->IsValidLowLevel())
	{
		AExileCharacter* P = Cast<AExileCharacter>(OwnerHUD->PlayerOwner->AcknowledgedPawn);
		if (P && P->FocusedUsableActor)
		{
			TArray<FName> Actions = P->FocusedUsableActor->GetActionList();

			if (Actions.Num() > 0)
			{
				FName Action = Actions[0];

				String = "Press [USE] to ";

				String.Append(Action.ToString());
			}

		}
	}
	return FText::FromString(String);
}

void SUIInteractionText::ToggleUseMenu(bool bOpen)
{
	bUseMenuOpen = bOpen;

	if (bUseMenuOpen)
	{
		// Change the UseMenuSlot Widget to the menu
		UseMenuSlot->Widget = GetUseMenu();
	}
	else
	{
		// Change the UseMenuSlot Widget to a null widget
		UseMenuSlot->Widget = SNullWidget::NullWidget;
	}
}

TSharedRef<SWidget> SUIInteractionText::GetUseMenu()
{
	// Create menu builder. Note that you dont have to pass optional parameters!
	FMenuBuilder MenuBuilder(true, NULL);

	if (OwnerHUD.IsValid() && OwnerHUD->PlayerOwner->IsValidLowLevel() && OwnerHUD->PlayerOwner->AcknowledgedPawn->IsValidLowLevel())
	{
		AExileCharacter* P = Cast<AExileCharacter>(OwnerHUD->PlayerOwner->AcknowledgedPawn);
		if (P && P->FocusedUsableActor)
		{
			TArray<FName> Actions = P->FocusedUsableActor->GetActionList();

			for (int32 i = 0; i < Actions.Num(); i++)
			{
				MenuBuilder.AddWidget(SNew(SButton)
					.Text(FText::FromString(Actions*.ToString()))
					.OnClicked(this, &SUIInteractionText::UseMenuButtonClick, Actions*)
					, FText::GetEmpty());
			}
		}
	}


	return  MenuBuilder.MakeWidget();
}

FReply SUIInteractionText::UseMenuButtonClick(FName Action)
{
	if (GEngine)
	{
		FString Msg = "Menu Item Clicked: ";
		Msg.Append(Action.ToString());
		GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Blue, Msg);
	}
	return FReply::Handled();
}

END_SLATE_FUNCTION_BUILD_OPTIMIZATION


How to cause a dynamic menu show up requires you to create a pointer to the slot using Expose and when you need to change that slot’s widget you can by setting the Widget variable to a new Widget Reference.

I am not sure if this is the best way to go about it but it seems to work and does not pause the game when the menu shows up. I will be showing a video of it in action when I get the rest of this system prototyped out.

Thanks again for your help and I hope this helps anyone that needs to use dynamic widget creation to change out a slot.

maybe you can share the result of this when you’re already done, at least a preview, so that we’ll know what happened… :slight_smile:

Yea, I am working on getting mouse control returned back to the game after clicking a button in the menu. I got keyboard control back but I have to click again to regain mouse control. Once I get that I can set up a a few different items with different menu items for each to show it working in game.

I can’t seem to figure out how to restore mouse control.

I am using



WidgetFocusedBeforeMe = FSlateApplication::Get().GetKeyboardFocusedWidget();
FSlateApplication::Get().SetKeyboardFocus(SharedThis(this), EKeyboardFocusCause::Keyboard);
FSlateApplication::Get().ReleaseMouseCapture();


to release mouse control so I can click on the menu button without having to first click to focus on the widget then click again to click on the button.

I have used



FSlateApplication::Get().ResetToDefaultInputSettings();
FSlateApplication::Get().SetKeyboardFocus(WidgetFocusedBeforeMe);
FSlateApplication::Get().SetFocusToGameViewport();


in all different combinations in hopes to get it to return back to normal after closing the menu with no luck so far.

I have it closing the menu just fine after clicking, however I only have keyboard control and no mouse control until I click and then it regains mouse movement for looking around.

This seems to be my last major hurdle for this. Once I can regain mouse control this is going to be really cool. I will start working on some temporary meshes to make it easier to see when I make a demonstration video of it working. I hope taking my mind off trying to salve this problem will cause me to figure it out and see how simple it actually was. At least I hope it is that simple lol.

Sorry for so many replies in short succession. Here is a video of my dynamic use menu in action where it currently is at. The actions do nothing currently except output a message to the upper left to confirm that it’s functioning and just needs to be connected up to actually call the action on the actor. I am still trying to fix the mouse control returning when the menu is closed. Any stuttering in the video is due to fraps as it runs fine until fraps starts recording and stutters for some reason.

https://www.youtube.com/watch?v=aBHJoyKO0aM

I think it is coming along nicely and I am happy its starting to function like I want.

I hope I can get it fully working and move onto getting the rest of the system functioning :slight_smile:

Its very nice to see nice neat and well written code for a change.