Dismiss SCompoundWidget from SButton Issue

I’m trying to toggle a ‘ChooseTeam’ menu in the ShooterHUD with a keyboard key and also provide a ‘Cancel’ button within the menu to close it out as well.

Here’s the code I use to Toggle it:



void AShooterHUD::ToggleChooseTeam()
{
	AShooterPlayerController* ShooterPC = Cast<AShooterPlayerController>(PlayerOwner);

	if (ShooterPC->IsGameMenuVisible())
	{
		return;
	}

	if (!bIsChooseTeamVisible)
	{
		bIsChooseTeamVisible = true;

		SAssignNew(ChooseTeamWidgetOverlay, SOverlay)
			+ SOverlay::Slot()
			.HAlign(EHorizontalAlignment::HAlign_Center)
			.VAlign(EVerticalAlignment::VAlign_Center)
			.Padding(FMargin(50))
			
				SAssignNew(ChooseTeamWidget, SShooterChooseTeamWidget)
				.PCOwner(TWeakObjectPtr<APlayerController>(PlayerOwner))
				.MatchState(GetMatchState())
				.Cursor(EMouseCursor::Default)
			];

		GEngine->GameViewport->AddViewportWidgetContent(
			SAssignNew(ChooseTeamWidgetContainer, SWeakWidget)
			.PossiblyNullContent(ChooseTeamWidgetOverlay));
		ChooseTeamWidget->ShowMouseCursor();
		ChooseTeamWidget->OnMenuHidden.BindUObject(this, &AShooterHUD::ToggleChooseTeam);

		FSlateApplication::Get().SetKeyboardFocus(ChooseTeamWidget);
	}
	else if (ChooseTeamWidgetContainer.IsValid())
	{
		bIsChooseTeamVisible = false;
		ChooseTeamWidget->HideMouseCursor();
		GEngine->GameViewport->RemoveViewportWidgetContent(ChooseTeamWidgetContainer.ToSharedRef());
		FSlateApplication::Get().SetFocusToGameViewport();
	}
	else{
		bIsChooseTeamVisible = false;
	}
}


As you can see I hooked up a delegate to ‘OnMenuHidden’ such that clicking the cancel button will result in a call to ToggleChooseTeam()

Both methods of dismissing the SCompoundWidget menu work great except for one small but annoying difference.

When the user toggles the menu with the keyboard key, it properly enables/disables mouse look so that they can move their cursor around the menu without their player’s pitch/yaw changing.

However, enabling it with the keyboard key and dismissing it by clicking the ‘Cancel’ button does not properly return mouse look to the user. If you click the cancel button, the menu properly calls the delegate and the SCompoundWidget is removed from the screen. However, the user cannot affect the pitch/yaw with their mouse input once the menu disappears. Only by firing or aiming (left/right clicking) does it regain control and the user can mouse look again.

I feel like this may be a very nuanced issue where the SButton click may be interfering with properly returning focus to the viewport?

I’ve provided my code below for my SShooterChooseTeamWidget, only been working with Slate for a few days now so please excuse some of the poor code.

I appreciate any and all suggestions!



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

#pragma once

#include "Slate.h"

//class declare
class SShooterChooseTeamWidget : public SCompoundWidget
{

	SLATE_BEGIN_ARGS(SShooterChooseTeamWidget)
	{}

	SLATE_ARGUMENT(TWeakObjectPtr<APlayerController>, PCOwner)

	SLATE_ATTRIBUTE(EShooterMatchState::Type, MatchState)

	SLATE_END_ARGS()

	/** needed for every widget */
	void Construct(const FArguments& InArgs);

	/** update PlayerState maps with every tick when choose team is shown */
	virtual void Tick(const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime) OVERRIDE;

	/** to have the mouse cursor show up at all times, we need the widget to handle all mouse events */
	virtual FReply OnMouseButtonDown(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) OVERRIDE;

	/** says that we can support keyboard focus */
	virtual bool SupportsKeyboardFocus() const OVERRIDE{ return true; }

	/** The menu sets up the appropriate mouse settings upon focus */
	virtual FReply OnKeyboardFocusReceived(const FGeometry& MyGeometry, const FKeyboardFocusEvent& InKeyboardFocusEvent) OVERRIDE;

	/** Shows the mouse cursor */
	void ShowMouseCursor();

	/** Hides the mouse cursor */
	void HideMouseCursor();

	/** delegate declaration */
	DECLARE_DELEGATE(FOnMenuHidden);

	/** delegate, which is executed when cancel button is clicked */
	FOnMenuHidden OnMenuHidden;
	
protected:

	/** updates widgets when players leave or join */
	void UpdateTeamInfo();

	/** choose team tint color */
	FLinearColor ChooseTeamTint;

	/** when the choose team was brought up. */
	double ChooseTeamStartTime;

	/** player count in each team in the last tick */
	TArray<int32> LastTeamPlayerCount;

	/** get state of current match */
	EShooterMatchState::Type MatchState;

	/** pointer to our parent HUD */
	TWeakObjectPtr<class APlayerController> PCOwner;

	/** style for the choose team */
	const struct FShooterChooseTeamStyle *ChooseTeamStyle;

	/*Pointer to resources in GameModule*/
	TSharedPtr<FSlateGameResources> MyUIResources;

	const FSlateBrush * GetTeamDescriptionImageBrush() const;

	TSharedPtr<SButton> Team1Button;

	TSharedPtr<SButton> Team2Button;

	/** callback for when one of the N buttons is clicked */
	FReply ButtonClicked(int32 ButtonIndex);
};




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

#include "ShooterGame.h"
#include "SShooterChooseTeamWidget.h"
#include "ShooterStyle.h"
#include "ShooterChooseTeamWidgetStyle.h"

#define LOCTEXT_NAMESPACE "ShooterChooseTeam"

void SShooterChooseTeamWidget::Construct(const FArguments& InArgs)
{
	////////////////////////////////////////////////////////////////////////////////////////////////////
	/////Get handle on game resources from GameModule
	MyUIResources = FModuleManager::LoadModuleChecked<FShooterGameModule>(FName("ShooterGame")).GetSlateGameResources();

	////////////////////////////////////////////////////////////////////////////////////////////////////
	/////Get handle on Slate Brush
	/////name: member_icon
	const FSlateBrush *m_allied_flag_icon = MyUIResources->GetBrush(FName("AlliedTeamIcon"));
	const FSlateBrush *m_axis_flag_icon = MyUIResources->GetBrush(FName("AxisTeamIcon"));
	const FSlateBrush *m_allied_description = MyUIResources->GetBrush(FName("AlliedTeamDescription"));
	const FSlateBrush *m_axis_description = MyUIResources->GetBrush(FName("AxisTeamDescription"));
	const FSlateBrush *m_map_description = MyUIResources->GetBrush(FName("MapDescription"));

	ChooseTeamStyle = &FShooterStyle::Get().GetWidgetStyle<FShooterChooseTeamStyle>("DefaultShooterChooseTeamStyle");

	PCOwner = InArgs._PCOwner;
	ChooseTeamTint = FLinearColor(0.0f, 0.0f, 0.0f, 0.4f);

	ChooseTeamStartTime = FPlatformTime::Seconds();
	MatchState = InArgs._MatchState.Get();

	FMargin ButtonPadding = FMargin(10);
	uint16 ButtonFontSize = 16;

	const TSharedRef<SVerticalBox> ChooseTeamMenu = SNew(SVerticalBox) + SVerticalBox::Slot().AutoHeight();

	/*################################# CREATE MENU ROWS #####################################################*/

	TSharedPtr<SHorizontalBox> TitleRow;

	//Create title row for 'Choose a Team' and 'Game Name' textblocks
	ChooseTeamMenu->AddSlot().AutoHeight().VAlign(VAlign_Center).HAlign(HAlign_Left)
	
		SAssignNew(TitleRow, SHorizontalBox)
	];

	TSharedPtr<SHorizontalBox> ButtonsRow;

	//Create button row for selecting Team 1, Team 2, Spectate, Auto-Assign
	ChooseTeamMenu->AddSlot().AutoHeight().VAlign(VAlign_Center).HAlign(HAlign_Left)
	
		SAssignNew(ButtonsRow, SHorizontalBox)
	];

	TSharedPtr<SHorizontalBox> DescriptionRow;

	//Create description row for the team and map
	ChooseTeamMenu->AddSlot().AutoHeight().VAlign(VAlign_Center).HAlign(HAlign_Left)
	
		SAssignNew(DescriptionRow, SHorizontalBox)
	];

	TSharedPtr<SHorizontalBox> AlliedCountHBox;
	TSharedPtr<SHorizontalBox> AxisCountHBox;

	//Create row to display count of players on each team
	ChooseTeamMenu->AddSlot().AutoHeight().VAlign(VAlign_Fill).HAlign(HAlign_Fill)
	
		SNew(SHorizontalBox)
		+ SHorizontalBox::Slot().HAlign(HAlign_Fill)
		
			SAssignNew(AlliedCountHBox, SHorizontalBox)
		]
		+ SHorizontalBox::Slot().HAlign(HAlign_Fill)
		
			SAssignNew(AxisCountHBox, SHorizontalBox)
		]
	];

	AlliedCountHBox->AddSlot().VAlign(VAlign_Fill).HAlign(HAlign_Left).AutoWidth()
	
		SNew(SImage).Image(m_allied_flag_icon)
	];
	AlliedCountHBox->AddSlot().VAlign(VAlign_Fill).HAlign(HAlign_Fill)
	
		SNew(STextBlock)
		.Text(LOCTEXT("AlliedTeamName", "American:").ToString())
		//.TextStyle(FShooterStyle::Get(), "ShooterGame.DefaultChooseTeam.Row.HeaderTextStyle")
		.Font(FSlateFontInfo(FPaths::EngineContentDir() / TEXT("Slate/Fonts/Roboto-Bold.ttf"), 16))
	];

	AxisCountHBox->AddSlot().VAlign(VAlign_Fill).HAlign(HAlign_Left).AutoWidth()
	
		SNew(SImage).Image(m_axis_flag_icon)
	];
	AxisCountHBox->AddSlot().VAlign(VAlign_Fill).HAlign(HAlign_Fill)
	
		SNew(STextBlock)
		.Text(LOCTEXT("AxisTeamName", "German:").ToString())
		//.TextStyle(FShooterStyle::Get(), "ShooterGame.DefaultChooseTeam.Row.HeaderTextStyle")
		.Font(FSlateFontInfo(FPaths::EngineContentDir() / TEXT("Slate/Fonts/Roboto-Bold.ttf"), 16))
	];

	TSharedPtr<SHorizontalBox> FooterRow;

	//Create footer row for cancel button
	ChooseTeamMenu->AddSlot().AutoHeight().VAlign(VAlign_Center).HAlign(HAlign_Left)
	
		SAssignNew(FooterRow, SHorizontalBox)
	];

	/*################################### POPULATE TITLE ROW #################################################*/

	TitleRow->AddSlot().VAlign(VAlign_Center).HAlign(HAlign_Left).AutoWidth().Padding(5)
	
		SNew(SHorizontalBox)
		+ SHorizontalBox::Slot().Padding(5)
		
			SNew(STextBlock)
			.Text(LOCTEXT("ChooseATeam", "CHOOSE A TEAM").ToString())
			//.TextStyle(FShooterStyle::Get(), "ShooterGame.DefaultChooseTeam.Row.HeaderTextStyle")
			.Font(FSlateFontInfo(FPaths::EngineContentDir() / TEXT("Slate/Fonts/Roboto-Bold.ttf"), 32))
		]
	];

	TitleRow->AddSlot().VAlign(VAlign_Center).HAlign(HAlign_Right).AutoWidth().Padding(5)
	
		SNew(SHorizontalBox)
		+ SHorizontalBox::Slot().Padding(5)
		
			SNew(STextBlock)
			.Text(LOCTEXT("GameName", "GameName").ToString())
			//.TextStyle(FShooterStyle::Get(), "ShooterGame.DefaultChooseTeam.Row.HeaderTextStyle")
			.Font(FSlateFontInfo(FPaths::EngineContentDir() / TEXT("Slate/Fonts/Roboto-Bold.ttf"), 32))
		]
	];

	/*################################### POPULATE BUTTON ROW ################################################*/

	//Add button to ButtonsRow for Team 1
	ButtonsRow->AddSlot().VAlign(VAlign_Center).HAlign(HAlign_Center).AutoWidth().Padding(5)
	
		SAssignNew(Team1Button,SButton)
		.ButtonStyle(FCoreStyle::Get(), "NoBorder")
		//.OnClicked(this, &SDDFileTree::RefreshButtonPressed)
		.HAlign(HAlign_Center)
		.VAlign(VAlign_Center)
		.ForegroundColor(FSlateColor::UseForeground())
		.OnClicked(this, &SShooterChooseTeamWidget::ButtonClicked,0)
		
			SNew(SBorder)
			.Padding(ButtonPadding)
			
				SNew(STextBlock)
				.Text(FString("Team 1"))
				.Font(FSlateFontInfo(FPaths::EngineContentDir() / TEXT("Slate/Fonts/Roboto-Bold.ttf"), ButtonFontSize))
			]
		]
	];

	//Add button to ButtonsRow for Team 2
	ButtonsRow->AddSlot().VAlign(VAlign_Center).HAlign(HAlign_Center).AutoWidth().Padding(5)
	
		SAssignNew(Team2Button, SButton)
		.ButtonStyle(FCoreStyle::Get(), "NoBorder")
		//.OnClicked(this, &SDDFileTree::RefreshButtonPressed)
		.HAlign(HAlign_Center)
		.VAlign(VAlign_Center)
		.ForegroundColor(FSlateColor::UseForeground())
		.OnClicked(this, &SShooterChooseTeamWidget::ButtonClicked, 1)
		
			SNew(SBorder)
			.Padding(ButtonPadding)
			
				SNew(STextBlock)
				.Text(FString("Team 2"))
				.Font(FSlateFontInfo(FPaths::EngineContentDir() / TEXT("Slate/Fonts/Roboto-Bold.ttf"), ButtonFontSize))
			]
		]
	];

	//Add button to ButtonsRow for 'Spectate'
	ButtonsRow->AddSlot().VAlign(VAlign_Center).HAlign(HAlign_Center).AutoWidth().Padding(5)
	
		SNew(SButton)
		.ButtonStyle(FCoreStyle::Get(), "NoBorder")
		//.OnClicked(this, &SDDFileTree::RefreshButtonPressed)
		.HAlign(HAlign_Center)
		.VAlign(VAlign_Center)
		.ForegroundColor(FSlateColor::UseForeground())
		.OnClicked(this, &SShooterChooseTeamWidget::ButtonClicked, 2)
		
			SNew(SBorder)
			.Padding(ButtonPadding)
			
				SNew(STextBlock)
				.Text(FString("3 Spectate"))
				.Font(FSlateFontInfo(FPaths::EngineContentDir() / TEXT("Slate/Fonts/Roboto-Bold.ttf"), ButtonFontSize))
			]
		]
	];

	//Add button to ButtonsRow for 'Auto-Assign'
	ButtonsRow->AddSlot().VAlign(VAlign_Center).HAlign(HAlign_Center).AutoWidth().Padding(5)
	
		SNew(SButton)
		.ButtonStyle(FCoreStyle::Get(), "NoBorder")
		//.OnClicked(this, &SDDFileTree::RefreshButtonPressed)
		.HAlign(HAlign_Center)
		.VAlign(VAlign_Center)
		.ForegroundColor(FSlateColor::UseForeground())
		.OnClicked(this, &SShooterChooseTeamWidget::ButtonClicked, 3)
		
			SNew(SBorder)
			.Padding(ButtonPadding)
			
				SNew(STextBlock)
				.Text(FString("4 Auto-Assign"))
				.Font(FSlateFontInfo(FPaths::EngineContentDir() / TEXT("Slate/Fonts/Roboto-Bold.ttf"), ButtonFontSize))
			]
		]
	];

	/*################################### POPULATE DESCRIPTION ROW ###########################################*/
	DescriptionRow->AddSlot().VAlign(VAlign_Center).HAlign(HAlign_Left).AutoWidth().Padding(5)
	
		SNew(SImage).Image(this, &SShooterChooseTeamWidget::GetTeamDescriptionImageBrush)
	];

	DescriptionRow->AddSlot().VAlign(VAlign_Center).HAlign(HAlign_Left).AutoWidth().Padding(5)
	
		SNew(SImage)
		.Image(m_map_description)
	];

	/*################################### POPULATE FOOTER ROW ################################################*/
	//Add cancel button to FooterRow
	FooterRow->AddSlot().VAlign(VAlign_Center).HAlign(HAlign_Center).AutoWidth().Padding(5)
	
		SNew(SButton)
		.ButtonStyle(FCoreStyle::Get(), "NoBorder")
		//.OnClicked(this, &SDDFileTree::RefreshButtonPressed)
		.HAlign(HAlign_Center)
		.VAlign(VAlign_Center)
		.ForegroundColor(FSlateColor::UseForeground())
		.OnClicked(this, &SShooterChooseTeamWidget::ButtonClicked, 4)
		
			SNew(SBorder)
			.Padding(ButtonPadding)
			
				SNew(STextBlock)
				.Text(FString("Cancel"))
				.Font(FSlateFontInfo(FPaths::EngineContentDir() / TEXT("Slate/Fonts/Roboto-Bold.ttf"), ButtonFontSize))
			]
		]
	];

	ChildSlot
		.VAlign(VAlign_Fill)
		.HAlign(HAlign_Fill)
		
			SNew(SBorder)
			.BorderImage(&ChooseTeamStyle->ItemBorderBrush)
			.BorderBackgroundColor(ChooseTeamTint)
			
				ChooseTeamMenu
			]
		];
}

void SShooterChooseTeamWidget::ShowMouseCursor()
{
	PCOwner->bShowMouseCursor = true;
	PCOwner->SetIgnoreLookInput(true);
}

void SShooterChooseTeamWidget::HideMouseCursor()
{
	PCOwner->bShowMouseCursor = false;
	PCOwner->SetIgnoreLookInput(false);
}

const FSlateBrush * SShooterChooseTeamWidget::GetTeamDescriptionImageBrush() const
{
	SButton * Team1ButtonPtr = Team1Button.Get();

	if (Team1ButtonPtr->IsHovered()){
		return MyUIResources->GetBrush(FName("AlliedTeamDescription"));
	}
	return MyUIResources->GetBrush(FName("AxisTeamDescription"));
}

FReply SShooterChooseTeamWidget::ButtonClicked(int32 ButtonIndex)
{
	UE_LOG(LogTemp, Warning, TEXT("SShooterChooseTeamWidget::ButtonClicked(int32 ButtonIndex): %d"),ButtonIndex);

	if (ButtonIndex == 4)
	{
		//Send event, so menu can be removed
		OnMenuHidden.ExecuteIfBound();
	}
	return FReply::Handled();
}

void SShooterChooseTeamWidget::Tick(const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime)
{
	SCompoundWidget::Tick(AllottedGeometry, InCurrentTime, InDeltaTime);
}

FReply SShooterChooseTeamWidget::OnKeyboardFocusReceived(const FGeometry& MyGeometry, const FKeyboardFocusEvent& InKeyboardFocusEvent)
{
	return FReply::Handled().ReleaseMouseCapture().CaptureJoystick(SharedThis(this), true);
}

FReply SShooterChooseTeamWidget::OnMouseButtonDown(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent)
{
	//Set the keyboard focus 
	return FReply::Handled()
		.SetKeyboardFocus(SharedThis(this), EKeyboardFocusCause::SetDirectly);
}

#undef LOCTEXT_NAMESPACE


I figured it out!

Changed this:



FReply SShooterChooseTeamWidget::ButtonClicked(int32 ButtonIndex)
{
	UE_LOG(LogTemp, Warning, TEXT("SShooterChooseTeamWidget::ButtonClicked(int32 ButtonIndex): %d"),ButtonIndex);

	if (ButtonIndex == 4)
	{
		//Send event, so menu can be removed
		OnMenuHidden.ExecuteIfBound();
	}
	return FReply::Handled();
}

void SShooterChooseTeamWidget::Tick(const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime)
{
	SCompoundWidget::Tick(AllottedGeometry, InCurrentTime, InDeltaTime);
}


to this:



FReply SShooterChooseTeamWidget::ButtonClicked(int32 ButtonIndex)
{
	UE_LOG(LogTemp, Warning, TEXT("SShooterChooseTeamWidget::ButtonClicked(int32 ButtonIndex): %d"),ButtonIndex);

	if (ButtonIndex == 4)
	{
            IsClosing = true;
	}
	return FReply::Handled();
}

void SShooterChooseTeamWidget::Tick(const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime)
{
	SCompoundWidget::Tick(AllottedGeometry, InCurrentTime, InDeltaTime);

        if(IsClosing)
        {
		//Send event, so menu can be removed
		OnMenuHidden.ExecuteIfBound();
        }
}


If anyone could explain why exactly this is the case I’d really appreciate it. My guess is that this defers the closing to the next tick which allows the ButtonClick handle to return properly.

Thanks you for this solution its working here too, I suppose the problem is that we try to change mouse focus inside a mouse event, something must go wrong with that. If you find a more nice solution please tell me ! :wink: