mirror of
https://github.com/sinbad/StevesUEHelpers.git
synced 2025-02-23 09:35:25 +00:00
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:
parent
918ccc5cc2
commit
a4f6647cf6
@ -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()
|
||||
|
75
Source/StevesUEHelpers/Private/StevesUI/FocusSystem.cpp
Normal file
75
Source/StevesUEHelpers/Private/StevesUI/FocusSystem.cpp
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
};
|
||||
|
17
Source/StevesUEHelpers/Public/StevesUI/FocusSystem.h
Normal file
17
Source/StevesUEHelpers/Public/StevesUI/FocusSystem.h
Normal 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);
|
||||
|
||||
};
|
@ -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;
|
||||
|
||||
};
|
||||
|
@ -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);
|
||||
|
@ -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();
|
||||
};
|
||||
|
@ -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);
|
||||
|
||||
};
|
Loading…
x
Reference in New Issue
Block a user