Auto-focus system now not dependent on MenuStack, just FocusableUserWidget

This allows you to have other FocusableUserWidget subclasses (e.g. single dialogs)
automatically figure out focus between each other
This commit is contained in:
Steve Streeting 2020-11-19 16:41:04 +00:00
parent 918ccc5cc2
commit a4f6647cf6
12 changed files with 189 additions and 147 deletions

View File

@ -87,9 +87,9 @@ void UStevesGameSubsystem::OnInputDetectorModeChanged(int PlayerIndex, EInputMod
OnInputModeChanged.Broadcast(PlayerIndex, NewMode);
}
FMenuSystem* UStevesGameSubsystem::GetMenuSystem()
FFocusSystem* UStevesGameSubsystem::GetFocusSystem()
{
return &MenuSystem;
return &FocusSystem;
}
UStevesGameSubsystem::FInputModeDetector::FInputModeDetector()

View File

@ -0,0 +1,75 @@
#include "StevesUI/FocusSystem.h"
#include "StevesUI/FocusableUserWidget.h"
DEFINE_LOG_CATEGORY(LogFocusSystem)
TWeakObjectPtr<UFocusableUserWidget> FFocusSystem::GetHighestFocusPriority()
{
int Highest = -999;
TWeakObjectPtr<UFocusableUserWidget> Ret;
for (auto && S : ActiveAutoFocusWidgets)
{
if (S.IsValid() && S->IsRequestingFocus() && S->GetAutomaticFocusPriority() > Highest)
{
Highest = S->GetAutomaticFocusPriority();
Ret = S;
}
}
return Ret;
}
void FFocusSystem::FocusableWidgetConstructed(UFocusableUserWidget* Widget)
{
UE_LOG(LogFocusSystem, Display, TEXT("FocusableUserWidget %s opened"), *Widget->GetName());
// check to make sure we never dupe, shouldn't normally be a problem
// but let's just be safe, there will never be that many
bool bPresent = false;
for (auto && S : ActiveAutoFocusWidgets)
{
if (S.Get() == Widget)
{
bPresent = true;
break;
}
}
if (!bPresent)
ActiveAutoFocusWidgets.Add(Widget);
if (Widget->IsRequestingFocus())
{
auto Highest = GetHighestFocusPriority();
if (!Highest.IsValid() || Highest->GetAutomaticFocusPriority() <= Widget->GetAutomaticFocusPriority())
{
// give new stack the focus if it's equal or higher priority than anything else
UE_LOG(LogFocusSystem, Display, TEXT("Giving focus to %s"), *Widget->GetName());
Widget->TakeFocusIfDesired();
}
}
}
void FFocusSystem::FocusableWidgetDestructed(UFocusableUserWidget* Widget)
{
UE_LOG(LogFocusSystem, Display, TEXT("FocusableUserWidget %s closed"), *Widget->GetName());
for (int i = 0; i < ActiveAutoFocusWidgets.Num(); ++i)
{
if (ActiveAutoFocusWidgets[i].Get() == Widget)
{
ActiveAutoFocusWidgets.RemoveAt(i);
break;
}
}
// if the menu closing had focus, give it to the highest remaining stack
if (Widget->HasFocusedDescendants())
{
auto Highest = GetHighestFocusPriority();
if (Highest.IsValid())
{
UE_LOG(LogFocusSystem, Display, TEXT("Giving focus to %s"), *Highest->GetName());
Highest->TakeFocusIfDesired();
}
}
}

View File

@ -1,7 +1,53 @@
#include "StevesUI/FocusableUserWidget.h"
#include "StevesUEHelpers.h"
void UFocusableUserWidget::SetFocusProperly_Implementation()
{
// Default is to call normal
SetFocus();
}
bool UFocusableUserWidget::IsRequestingFocus_Implementation() const
{
// Subclasses can override this
return bEnableAutomaticFocus;
}
bool UFocusableUserWidget::TakeFocusIfDesired_Implementation()
{
auto GS = GetStevesGameSubsystem(GetWorld());
if (IsRequestingFocus() &&
GS && (GS->GetLastInputModeUsed() != EInputMode::Gamepad || GS->GetLastInputModeUsed() != EInputMode::Keyboard))
{
SetFocusProperly();
return true;
}
return false;
}
void UFocusableUserWidget::NativeConstruct()
{
Super::NativeConstruct();
if (bEnableAutomaticFocus)
{
auto GS = GetStevesGameSubsystem(GetWorld());
if (GS)
GS->GetFocusSystem()->FocusableWidgetConstructed(this);
}
}
void UFocusableUserWidget::NativeDestruct()
{
Super::NativeDestruct();
if (bEnableAutomaticFocus)
{
auto GS = GetStevesGameSubsystem(GetWorld());
if (GS)
GS->GetFocusSystem()->FocusableWidgetDestructed(this);
}
}

View File

@ -134,14 +134,3 @@ void UMenuBase::Open(bool bIsRegain)
TakeFocusIfDesired();
}
void UMenuBase::TakeFocusIfDesired()
{
auto GS = GetStevesGameSubsystem(GetWorld());
if (bRequestFocus &&
GS && (GS->GetLastInputModeUsed() != EInputMode::Gamepad || GS->GetLastInputModeUsed() != EInputMode::Keyboard))
{
SetFocusProperly();
}
}

View File

@ -230,10 +230,7 @@ void UMenuStack::PopMenuIfTop(UMenuBase* UiMenuBase, bool bWasCancel)
void UMenuStack::FirstMenuOpened()
{
// tell menu system
auto GS = GetStevesGameSubsystem(GetWorld());
if (GS)
GS->GetMenuSystem()->MenuStackOpened(this);
// Nothing to do now but keep for future use
}
void UMenuStack::RemoveFromParent()
@ -247,11 +244,12 @@ void UMenuStack::RemoveFromParent()
Super::RemoveFromParent();
// tell menu system if we're in-game (this gets called in editor too)
auto GS = GetStevesGameSubsystem(GetWorld());
if (GS)
GS->GetMenuSystem()->MenuStackClosed(this);
}
UMenuStack::UMenuStack()
{
// Default to enabling automatic focus for menus (can still be overridden in serialized properties)
bEnableAutomaticFocus = true;
}
void UMenuStack::LastMenuClosed(bool bWasCancel)
@ -277,19 +275,12 @@ void UMenuStack::CloseAll(bool bWasCancel)
LastMenuClosed(bWasCancel);
}
bool UMenuStack::IsRequestingFocus() const
bool UMenuStack::IsRequestingFocus_Implementation() const
{
// Delegate to top menu
return Menus.Num() > 0 && Menus.Last()->IsRequestingFocus();
}
void UMenuStack::TakeFocusIfDesired()
{
if (IsRequestingFocus())
{
Menus.Last()->TakeFocusIfDesired();
}
}
void UMenuStack::SetFocusProperly_Implementation()
{
// Delegate to top menu

View File

@ -1,75 +0,0 @@
#include "StevesUI/MenuSystem.h"
#include "StevesUI/MenuStack.h"
DEFINE_LOG_CATEGORY(LogMenuSystem)
TWeakObjectPtr<UMenuStack> FMenuSystem::GetHighestFocusPriority()
{
int Highest = -999;
TWeakObjectPtr<UMenuStack> Ret;
for (auto && S : ActiveMenuStacks)
{
if (S.IsValid() && S->IsRequestingFocus() && S->FocusPriority > Highest)
{
Highest = S->FocusPriority;
Ret = S;
}
}
return Ret;
}
void FMenuSystem::MenuStackOpened(UMenuStack* Stack)
{
UE_LOG(LogMenuSystem, Display, TEXT("MenuStack %s opened"), *Stack->GetName());
// check to make sure we never dupe, shouldn't normally be a problem
// but let's just be safe, there will never be that many
bool bPresent = false;
for (auto && S : ActiveMenuStacks)
{
if (S.Get() == Stack)
{
bPresent = true;
break;
}
}
if (!bPresent)
ActiveMenuStacks.Add(Stack);
if (Stack->IsRequestingFocus())
{
auto Highest = GetHighestFocusPriority();
if (!Highest.IsValid() || Highest->FocusPriority <= Stack->FocusPriority)
{
// give new stack the focus if it's equal or higher priority than anything else
UE_LOG(LogMenuSystem, Display, TEXT("Giving focus to MenuStack %s"), *Stack->GetName());
Stack->TakeFocusIfDesired();
}
}
}
void FMenuSystem::MenuStackClosed(UMenuStack* Stack)
{
UE_LOG(LogMenuSystem, Display, TEXT("MenuStack %s closed"), *Stack->GetName());
for (int i = 0; i < ActiveMenuStacks.Num(); ++i)
{
if (ActiveMenuStacks[i].Get() == Stack)
{
ActiveMenuStacks.RemoveAt(i);
break;
}
}
// if the menu closing had focus, give it to the highest remaining stack
if (Stack->HasFocusedDescendants())
{
auto Highest = GetHighestFocusPriority();
if (Highest.IsValid())
{
UE_LOG(LogMenuSystem, Display, TEXT("Giving focus to MenuStack %s"), *Highest->GetName());
Highest->TakeFocusIfDesired();
}
}
}

View File

@ -5,7 +5,7 @@
#include "InputCoreTypes.h"
#include "Framework/Application/IInputProcessor.h"
#include "StevesHelperCommon.h"
#include "StevesUI/MenuSystem.h"
#include "StevesUI/FocusSystem.h"
#include "StevesUI/UiTheme.h"
#include "StevesGameSubsystem.generated.h"
@ -92,7 +92,7 @@ protected:
protected:
TSharedPtr<FInputModeDetector> InputDetector;
FMenuSystem MenuSystem;
FFocusSystem FocusSystem;
UPROPERTY(BlueprintReadWrite)
UUiTheme* DefaultUiTheme;
@ -123,6 +123,6 @@ public:
/// Changes the default theme to a different one
void SetDefaultUiTheme(UUiTheme* NewTheme) { DefaultUiTheme = NewTheme; }
/// Get the global menu system
FMenuSystem* GetMenuSystem();
/// Get the global focus system
FFocusSystem* GetFocusSystem();
};

View File

@ -0,0 +1,17 @@
#pragma once
#include "CoreMinimal.h"
DECLARE_LOG_CATEGORY_EXTERN(LogFocusSystem, Log, All)
class FFocusSystem
{
protected:
TArray<TWeakObjectPtr<class UFocusableUserWidget>> ActiveAutoFocusWidgets;
TWeakObjectPtr<UFocusableUserWidget> GetHighestFocusPriority();
public:
void FocusableWidgetConstructed(UFocusableUserWidget* Widget);
void FocusableWidgetDestructed(UFocusableUserWidget* Widget);
};

View File

@ -11,10 +11,40 @@ class STEVESUEHELPERS_API UFocusableUserWidget : public UUserWidget
{
GENERATED_BODY()
protected:
/// If enabled, this widget will opt-in to the list of widgets which can be given focus
/// automatically when another UFocusableUserWidget with focus is removed from the viewport.
/// Useful for making sure something has the focus at all times without having to have cross-dependencies
/// between UI parts, or events everywhere
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Focus")
bool bEnableAutomaticFocus = false;
/// If bEnableAutomaticFocus is enabled, then this is the focus priority associated with this widget.
/// In the event that there is more than one auto focus widget available, the highest priority widget will win.
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Focus")
int AutomaticFocusPriority = 0;
public:
/// UWidget::SetFocus is not virtual FFS. This does the same as SetFocus by default but can be overridden,
/// e.g. to delegate focus to specific children
UFUNCTION(BlueprintNativeEvent, BlueprintCallable)
void SetFocusProperly();
/// Whether this widget is *currently* requesting focus. Default is to use IsAutomaticFocusEnabled but subclasses
/// may override this to be volatile
UFUNCTION(BlueprintNativeEvent, BlueprintCallable)
bool IsRequestingFocus() const;
/// Tell this widget to take the focus if it desires to
UFUNCTION(BlueprintNativeEvent, BlueprintCallable)
bool TakeFocusIfDesired();
virtual bool IsAutomaticFocusEnabled() const { return bEnableAutomaticFocus; }
virtual int GetAutomaticFocusPriority() const { return AutomaticFocusPriority; }
protected:
virtual void NativeConstruct() override;
virtual void NativeDestruct() override;
};

View File

@ -76,13 +76,8 @@ public:
UFUNCTION(BlueprintCallable)
void Close(bool bWasCancel);
/// Triggers this menu to take focus if appropriate
/// This means if the menu both requests focus, and gamepad or keyboard is in use
UFUNCTION(BlueprintCallable)
void TakeFocusIfDesired();
TWeakObjectPtr<UMenuStack> GetParentStack() const { return ParentStack; }
bool IsRequestingFocus() const { return bRequestFocus; }
virtual bool IsRequestingFocus_Implementation() const override { return bRequestFocus; }
void AddedToStack(UMenuStack* Parent);
void RemovedFromStack(UMenuStack* Parent);

View File

@ -53,12 +53,6 @@ protected:
void InputModeChanged(int PlayerIndex, EInputMode NewMode);
public:
/// The focus priority of this stack compared to others. When a MenuStack is opened, if it has higher priority than
/// any existing MenuStack open, it will be given focus. When a MenuStack with focus is closed, the next highest
/// priority one will be given focus.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Behaviour")
int FocusPriority = 0;
/// Input keys which go back a level in the menu stack (default Esc and B gamepad button)
/// Clear this list if you don't want this behaviour
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Input")
@ -129,15 +123,12 @@ public:
void CloseAll(bool bWasCancel);
/// Whether the top MenuBase on this stack is requesting focus
UFUNCTION(BlueprintCallable)
bool IsRequestingFocus() const;
/// Triggers this stack to take focus (specifically its topmost MenuBase) if appropriate
/// This means if both top menu on the stack requests focus, and gamepad or keyboard is in use
UFUNCTION(BlueprintCallable)
void TakeFocusIfDesired();
virtual bool IsRequestingFocus_Implementation() const override;
virtual void SetFocusProperly_Implementation() override;
virtual void PopMenuIfTop(UMenuBase* UiMenuBase, bool bWasCancel);
virtual void RemoveFromParent() override;
UMenuStack();
};

View File

@ -1,17 +0,0 @@
#pragma once
#include "CoreMinimal.h"
DECLARE_LOG_CATEGORY_EXTERN(LogMenuSystem, Log, All)
class FMenuSystem
{
protected:
TArray<TWeakObjectPtr<class UMenuStack>> ActiveMenuStacks;
TWeakObjectPtr<UMenuStack> GetHighestFocusPriority();
public:
void MenuStackOpened(UMenuStack* Stack);
void MenuStackClosed(UMenuStack* Stack);
};