From a4f6647cf60a4d9b8c30ce7cd67e44e1ae7ff47f Mon Sep 17 00:00:00 2001 From: Steve Streeting Date: Thu, 19 Nov 2020 16:41:04 +0000 Subject: [PATCH] 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 --- .../Private/StevesGameSubsystem.cpp | 4 +- .../Private/StevesUI/FocusSystem.cpp | 75 +++++++++++++++++++ .../Private/StevesUI/FocusableUserWidget.cpp | 46 ++++++++++++ .../Private/StevesUI/MenuBase.cpp | 11 --- .../Private/StevesUI/MenuStack.cpp | 27 +++---- .../Private/StevesUI/MenuSystem.cpp | 75 ------------------- .../Public/StevesGameSubsystem.h | 8 +- .../Public/StevesUI/FocusSystem.h | 17 +++++ .../Public/StevesUI/FocusableUserWidget.h | 32 +++++++- .../Public/StevesUI/MenuBase.h | 7 +- .../Public/StevesUI/MenuStack.h | 17 +---- .../Public/StevesUI/MenuSystem.h | 17 ----- 12 files changed, 189 insertions(+), 147 deletions(-) create mode 100644 Source/StevesUEHelpers/Private/StevesUI/FocusSystem.cpp delete mode 100644 Source/StevesUEHelpers/Private/StevesUI/MenuSystem.cpp create mode 100644 Source/StevesUEHelpers/Public/StevesUI/FocusSystem.h delete mode 100644 Source/StevesUEHelpers/Public/StevesUI/MenuSystem.h diff --git a/Source/StevesUEHelpers/Private/StevesGameSubsystem.cpp b/Source/StevesUEHelpers/Private/StevesGameSubsystem.cpp index 1166b14..ed285e7 100644 --- a/Source/StevesUEHelpers/Private/StevesGameSubsystem.cpp +++ b/Source/StevesUEHelpers/Private/StevesGameSubsystem.cpp @@ -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() diff --git a/Source/StevesUEHelpers/Private/StevesUI/FocusSystem.cpp b/Source/StevesUEHelpers/Private/StevesUI/FocusSystem.cpp new file mode 100644 index 0000000..3b8d339 --- /dev/null +++ b/Source/StevesUEHelpers/Private/StevesUI/FocusSystem.cpp @@ -0,0 +1,75 @@ +#include "StevesUI/FocusSystem.h" +#include "StevesUI/FocusableUserWidget.h" + +DEFINE_LOG_CATEGORY(LogFocusSystem) + +TWeakObjectPtr FFocusSystem::GetHighestFocusPriority() +{ + int Highest = -999; + TWeakObjectPtr 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(); + } + } +} diff --git a/Source/StevesUEHelpers/Private/StevesUI/FocusableUserWidget.cpp b/Source/StevesUEHelpers/Private/StevesUI/FocusableUserWidget.cpp index f5669c5..cd339af 100644 --- a/Source/StevesUEHelpers/Private/StevesUI/FocusableUserWidget.cpp +++ b/Source/StevesUEHelpers/Private/StevesUI/FocusableUserWidget.cpp @@ -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); + } + +} diff --git a/Source/StevesUEHelpers/Private/StevesUI/MenuBase.cpp b/Source/StevesUEHelpers/Private/StevesUI/MenuBase.cpp index 3198d63..aa0bbee 100644 --- a/Source/StevesUEHelpers/Private/StevesUI/MenuBase.cpp +++ b/Source/StevesUEHelpers/Private/StevesUI/MenuBase.cpp @@ -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(); - } -} - diff --git a/Source/StevesUEHelpers/Private/StevesUI/MenuStack.cpp b/Source/StevesUEHelpers/Private/StevesUI/MenuStack.cpp index 9f4e192..7175ebf 100644 --- a/Source/StevesUEHelpers/Private/StevesUI/MenuStack.cpp +++ b/Source/StevesUEHelpers/Private/StevesUI/MenuStack.cpp @@ -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 diff --git a/Source/StevesUEHelpers/Private/StevesUI/MenuSystem.cpp b/Source/StevesUEHelpers/Private/StevesUI/MenuSystem.cpp deleted file mode 100644 index 5122cd8..0000000 --- a/Source/StevesUEHelpers/Private/StevesUI/MenuSystem.cpp +++ /dev/null @@ -1,75 +0,0 @@ -#include "StevesUI/MenuSystem.h" -#include "StevesUI/MenuStack.h" - -DEFINE_LOG_CATEGORY(LogMenuSystem) - -TWeakObjectPtr FMenuSystem::GetHighestFocusPriority() -{ - int Highest = -999; - TWeakObjectPtr 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(); - } - } -} diff --git a/Source/StevesUEHelpers/Public/StevesGameSubsystem.h b/Source/StevesUEHelpers/Public/StevesGameSubsystem.h index d15a76f..553bab0 100644 --- a/Source/StevesUEHelpers/Public/StevesGameSubsystem.h +++ b/Source/StevesUEHelpers/Public/StevesGameSubsystem.h @@ -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 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(); }; diff --git a/Source/StevesUEHelpers/Public/StevesUI/FocusSystem.h b/Source/StevesUEHelpers/Public/StevesUI/FocusSystem.h new file mode 100644 index 0000000..ab484a5 --- /dev/null +++ b/Source/StevesUEHelpers/Public/StevesUI/FocusSystem.h @@ -0,0 +1,17 @@ +#pragma once + +#include "CoreMinimal.h" + +DECLARE_LOG_CATEGORY_EXTERN(LogFocusSystem, Log, All) + +class FFocusSystem +{ +protected: + TArray> ActiveAutoFocusWidgets; + + TWeakObjectPtr GetHighestFocusPriority(); +public: + void FocusableWidgetConstructed(UFocusableUserWidget* Widget); + void FocusableWidgetDestructed(UFocusableUserWidget* Widget); + +}; diff --git a/Source/StevesUEHelpers/Public/StevesUI/FocusableUserWidget.h b/Source/StevesUEHelpers/Public/StevesUI/FocusableUserWidget.h index 5ee33dc..6c14528 100644 --- a/Source/StevesUEHelpers/Public/StevesUI/FocusableUserWidget.h +++ b/Source/StevesUEHelpers/Public/StevesUI/FocusableUserWidget.h @@ -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; + }; diff --git a/Source/StevesUEHelpers/Public/StevesUI/MenuBase.h b/Source/StevesUEHelpers/Public/StevesUI/MenuBase.h index 406eae5..658fb07 100644 --- a/Source/StevesUEHelpers/Public/StevesUI/MenuBase.h +++ b/Source/StevesUEHelpers/Public/StevesUI/MenuBase.h @@ -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 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); diff --git a/Source/StevesUEHelpers/Public/StevesUI/MenuStack.h b/Source/StevesUEHelpers/Public/StevesUI/MenuStack.h index c8d866a..7557429 100644 --- a/Source/StevesUEHelpers/Public/StevesUI/MenuStack.h +++ b/Source/StevesUEHelpers/Public/StevesUI/MenuStack.h @@ -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(); }; diff --git a/Source/StevesUEHelpers/Public/StevesUI/MenuSystem.h b/Source/StevesUEHelpers/Public/StevesUI/MenuSystem.h deleted file mode 100644 index 5715839..0000000 --- a/Source/StevesUEHelpers/Public/StevesUI/MenuSystem.h +++ /dev/null @@ -1,17 +0,0 @@ -#pragma once - -#include "CoreMinimal.h" - -DECLARE_LOG_CATEGORY_EXTERN(LogMenuSystem, Log, All) - -class FMenuSystem -{ -protected: - TArray> ActiveMenuStacks; - - TWeakObjectPtr GetHighestFocusPriority(); -public: - void MenuStackOpened(UMenuStack* Stack); - void MenuStackClosed(UMenuStack* Stack); - -};