From e6492ea0e3b50a82c41134425461a63151374c7c Mon Sep 17 00:00:00 2001 From: Jamie Greunbaum Date: Mon, 13 Mar 2023 15:58:22 -0400 Subject: [PATCH] Initial commit. --- ControllerMenu.uplugin | 20 + Source/ControllerMenu/ControllerMenu.Build.cs | 49 + .../ControllerMenu/Private/ControllerMenu.cpp | 44 + .../Private/ControllerMenuGlobalSettings.cpp | 47 + .../Private/ControllerMenuPrivatePCH.h | 14 + .../Widgets/Components/ControllerButton.cpp | 173 ++ .../Widgets/Components/TextSpinner.cpp | 476 ++++++ .../ControllerMenuMessageBoxWidget.cpp | 58 + .../Private/Widgets/ControllerMenuWidget.cpp | 1408 +++++++++++++++++ .../Public/ControllerMenuGlobalSettings.h | 147 ++ .../Public/IControllerMenu_Implementation.h | 45 + .../Public/Interfaces/ITakesControllerFocus.h | 49 + .../Widgets/Components/ControllerButton.h | 137 ++ .../Public/Widgets/Components/TextSpinner.h | 335 ++++ .../Widgets/ControllerMenuMessageBoxWidget.h | 43 + .../Public/Widgets/ControllerMenuWidget.h | 346 ++++ .../Public/Widgets/Slate/SCustomTextBlock.h | 29 + 17 files changed, 3420 insertions(+) create mode 100644 ControllerMenu.uplugin create mode 100644 Source/ControllerMenu/ControllerMenu.Build.cs create mode 100644 Source/ControllerMenu/Private/ControllerMenu.cpp create mode 100644 Source/ControllerMenu/Private/ControllerMenuGlobalSettings.cpp create mode 100644 Source/ControllerMenu/Private/ControllerMenuPrivatePCH.h create mode 100644 Source/ControllerMenu/Private/Widgets/Components/ControllerButton.cpp create mode 100644 Source/ControllerMenu/Private/Widgets/Components/TextSpinner.cpp create mode 100644 Source/ControllerMenu/Private/Widgets/ControllerMenuMessageBoxWidget.cpp create mode 100644 Source/ControllerMenu/Private/Widgets/ControllerMenuWidget.cpp create mode 100644 Source/ControllerMenu/Public/ControllerMenuGlobalSettings.h create mode 100644 Source/ControllerMenu/Public/IControllerMenu_Implementation.h create mode 100644 Source/ControllerMenu/Public/Interfaces/ITakesControllerFocus.h create mode 100644 Source/ControllerMenu/Public/Widgets/Components/ControllerButton.h create mode 100644 Source/ControllerMenu/Public/Widgets/Components/TextSpinner.h create mode 100644 Source/ControllerMenu/Public/Widgets/ControllerMenuMessageBoxWidget.h create mode 100644 Source/ControllerMenu/Public/Widgets/ControllerMenuWidget.h create mode 100644 Source/ControllerMenu/Public/Widgets/Slate/SCustomTextBlock.h diff --git a/ControllerMenu.uplugin b/ControllerMenu.uplugin new file mode 100644 index 0000000..9d1640d --- /dev/null +++ b/ControllerMenu.uplugin @@ -0,0 +1,20 @@ +{ + "FileVersion": 3, + "Version": 1.0, + "VersionName": "1.0", + "FriendlyName": "ControllerMenu", + "Description": "Custom class that allows easy setup of hybrid controller/keyboard/mouse menu navigation.", + "Category": "Menu", + "CreatedBy": "Batty Bovine Productions, LLC", + "CreatedByURL": "https://battybovine.com", + "EnabledByDefault": true, + "CanContainContent": true, + "IsBetaVersion": true, + "Modules": [ + { + "Name": "ControllerMenu", + "Type": "Runtime", + "LoadingPhase": "PreDefault" + } + ] +} \ No newline at end of file diff --git a/Source/ControllerMenu/ControllerMenu.Build.cs b/Source/ControllerMenu/ControllerMenu.Build.cs new file mode 100644 index 0000000..31eb76a --- /dev/null +++ b/Source/ControllerMenu/ControllerMenu.Build.cs @@ -0,0 +1,49 @@ +/** + * Copyright ©2019 Batty Bovine Productions, LLC. All Rights Reserved. + **/ + + +using System.IO; + +namespace UnrealBuildTool.Rules +{ + public class ControllerMenu : ModuleRules + { + public ControllerMenu(ReadOnlyTargetRules Target) : base(Target) + { + bLegacyPublicIncludePaths = true; + PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs; + ShadowVariableWarningLevel = WarningLevel.Warning; + + PublicIncludePaths.Add(Path.Combine(ModuleDirectory, "Public")); + + PrivateIncludePaths.AddRange( + new string[] + { + "ControllerMenu/Private" + } + ); + + PublicDependencyModuleNames.AddRange( + new string[] + { + "Core", + "CoreUObject", + "Engine", + "InputCore", + "UMG", + "DeveloperSettings" + } + ); + + PrivateDependencyModuleNames.AddRange( + new string[] + { + "Slate", + "SlateCore" + } + ); + } + } +} +/// \ No newline at end of file diff --git a/Source/ControllerMenu/Private/ControllerMenu.cpp b/Source/ControllerMenu/Private/ControllerMenu.cpp new file mode 100644 index 0000000..612daac --- /dev/null +++ b/Source/ControllerMenu/Private/ControllerMenu.cpp @@ -0,0 +1,44 @@ +/** + * Copyright ©2019 Batty Bovine Productions, LLC. All Rights Reserved. + **/ + + +#include "CoreMinimal.h" +#include "Engine.h" +#include "Modules/ModuleManager.h" + +#include "IControllerMenu_Implementation.h" + + +DEFINE_LOG_CATEGORY(ControllerMenuLog); + +#define LOCTEXT_NAMESPACE "ControllerMenu" + + +class FControllerMenu : public IControllerMenu_Implementation +{ + /** IModuleInterface implementation */ + virtual void StartupModule() override; + virtual void ShutdownModule() override; + // End IModuleInterface implementation + +}; + +void FControllerMenu::StartupModule() +{ + // This code will execute after your module is loaded into memory (but after global variables are initialized, of course.) + UE_LOG(ControllerMenuLog, Warning, TEXT("ControllerMenu: Log Started")); + +} + +void FControllerMenu::ShutdownModule() +{ + // This function may be called during shutdown to clean up your module. For modules that support dynamic reloading, + // we call this function before unloading the module. + UE_LOG(ControllerMenuLog, Warning, TEXT("ControllerMenu: Log Ended")); + +} + +IMPLEMENT_MODULE(FControllerMenu, ControllerMenu) + +#undef LOCTEXT_NAMESPACE diff --git a/Source/ControllerMenu/Private/ControllerMenuGlobalSettings.cpp b/Source/ControllerMenu/Private/ControllerMenuGlobalSettings.cpp new file mode 100644 index 0000000..d026ba1 --- /dev/null +++ b/Source/ControllerMenu/Private/ControllerMenuGlobalSettings.cpp @@ -0,0 +1,47 @@ +/** +* Copyright ©2019 BattyBovine +* +* Permission is hereby granted, free of charge, to any person obtaining a +* copy of this software and associated documentation files (the "Software"), +* to deal in the Software without restriction, including without limitation +* the rights to use, copy, modify, merge, publish, distribute, sublicense, +* and/or sell copies of the Software, and to permit persons to whom the +* Software is furnished to do so, subject to the following conditions: + +* The above copyright notice and this permission notice shall be included in +* all copies or substantial portions of the Software. + +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +* DEALINGS IN THE SOFTWARE. +**/ + + +#include "ControllerMenuGlobalSettings.h" + + +void UControllerMenuGlobalSettings::PostInitProperties() +{ + Super::PostInitProperties(); +} + +FName UControllerMenuGlobalSettings::GetCategoryName() const +{ + return FName(TEXT("Plugins")); +} + +#if WITH_EDITOR +void UControllerMenuGlobalSettings::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) +{ + Super::PostEditChangeProperty(PropertyChangedEvent); + + if (PropertyChangedEvent.Property) + { + this->ExportValuesToConsoleVariables(PropertyChangedEvent.Property); + } +} +#endif diff --git a/Source/ControllerMenu/Private/ControllerMenuPrivatePCH.h b/Source/ControllerMenu/Private/ControllerMenuPrivatePCH.h new file mode 100644 index 0000000..05840cf --- /dev/null +++ b/Source/ControllerMenu/Private/ControllerMenuPrivatePCH.h @@ -0,0 +1,14 @@ +/** + * Copyright ©2019 Batty Bovine Productions, LLC. All Rights Reserved. + **/ + + +#include "CoreUObject.h" +#include "Engine.h" +#include "ModuleManager.h" +#include "Slate.h" +#include "UnrealEd.h" + +// You should place include statements to your module's private header files here. You only need to +// add includes for headers that are used in most of your module's source files though. +#include "IControllerMenu_Implementation.h" diff --git a/Source/ControllerMenu/Private/Widgets/Components/ControllerButton.cpp b/Source/ControllerMenu/Private/Widgets/Components/ControllerButton.cpp new file mode 100644 index 0000000..66606fd --- /dev/null +++ b/Source/ControllerMenu/Private/Widgets/Components/ControllerButton.cpp @@ -0,0 +1,173 @@ +/** + * Copyright ©2019 Batty Bovine Productions, LLC. All Rights Reserved. + **/ + + +#include "ControllerButton.h" + +#include "ConstructorHelpers.h" + +#include "Components/ButtonSlot.h" +#include "Components/TextBlock.h" +#include "Engine/Font.h" +#include "Framework/Application/SlateApplication.h" +#include "Input/SButton.h" +#include "Misc/ConfigCacheIni.h" +#include "Text/STextBlock.h" +#include "Widgets/SBoxPanel.h" + + +#define LOCTEXT_NAMESPACE "ControllerButton" + + +UControllerButton::UControllerButton(const FObjectInitializer& ObjectInitializer) : + Super(ObjectInitializer) +{ + this->bIsVariable = true; + + if (this->DefaultControllerButtonStyle == nullptr) + { + // HACK: THIS SHOULD NOT COME FROM CORESTYLE AND SHOULD INSTEAD BE DEFINED BY ENGINE TEXTURES/PROJECT SETTINGS + this->DefaultControllerButtonStyle = new FButtonStyle(FCoreStyle::Get().GetWidgetStyle("Button")); + + // Unlink UMG default colors from the editor settings colors. + this->DefaultControllerButtonStyle->UnlinkColors(); + } + this->ButtonStyle = *this->DefaultControllerButtonStyle; + + this->ClickMethod = EButtonClickMethod::DownAndUp; + this->TouchMethod = EButtonTouchMethod::DownAndUp; + this->PressMethod = EButtonPressMethod::ButtonPress; +} + +TSharedRef UControllerButton::RebuildWidget() +{ + this->SlateButton = SNew(SButton) + .OnClicked(BIND_UOBJECT_DELEGATE(FOnClicked, SlateHandleClicked)) + .OnPressed(BIND_UOBJECT_DELEGATE(FSimpleDelegate, SlateHandlePressed)) + .OnReleased(BIND_UOBJECT_DELEGATE(FSimpleDelegate, SlateHandleReleased)) + .OnHovered_UObject(this, &UControllerButton::SlateHandleHovered) + .OnUnhovered_UObject(this, &UControllerButton::SlateHandleUnhovered) + .ButtonStyle(&this->ButtonStyle) + .ClickMethod(this->ClickMethod) + .TouchMethod(this->TouchMethod) + .PressMethod(this->PressMethod) + .HAlign(EHorizontalAlignment::HAlign_Fill) + .VAlign(EVerticalAlignment::VAlign_Fill) + .IsEnabled(this->GetIsEnabled()) + .IsFocusable(this->bIsFocusable); + + if (this->GetChildrenCount() > 0) + { + Cast(this->GetContentSlot())->BuildSlot(SlateButton.ToSharedRef()); + } + + // Cache our normal style settings for programmatically changing the style later + this->NormalBrush = this->ButtonStyle.Normal; + this->NormalPadding = this->ButtonStyle.NormalPadding; + + return this->SlateButton.ToSharedRef(); +} + +//void UControllerButton::SynchronizeProperties() +//{ +// Super::SynchronizeProperties(); +//} + +void UControllerButton::ReleaseSlateResources(bool bReleaseChildren) +{ + Super::ReleaseSlateResources(bReleaseChildren); + + this->SlateButton.Reset(); +} + +UClass* UControllerButton::GetSlotClass() const +{ + return UButtonSlot::StaticClass(); +} + +void UControllerButton::OnSlotAdded(UPanelSlot* InSlot) +{ + // Add the child to the live slot if it already exists + if (this->SlateButton.IsValid()) + { + CastChecked(InSlot)->BuildSlot(this->SlateButton.ToSharedRef()); + } +} + +void UControllerButton::OnSlotRemoved(UPanelSlot* InSlot) +{ + // Remove the widget from the live slot if it exists. + if (this->SlateButton.IsValid()) + { + this->SlateButton->SetContent(SNullWidget::NullWidget); + } +} + + +FReply UControllerButton::SlateHandleClicked() +{ + this->OnClicked.Broadcast(); + + return FReply::Handled(); +} + +void UControllerButton::SlateHandlePressed() +{ + this->OnPressed.Broadcast(); +} + +void UControllerButton::SlateHandleReleased() +{ + this->OnReleased.Broadcast(); +} + +void UControllerButton::SlateHandleHovered() +{ + this->OnHovered.Broadcast(); +} + +void UControllerButton::SlateHandleUnhovered() +{ + this->OnUnhovered.Broadcast(); +} + + +/** BEGIN ITakesControllerFocus implementation */ +void UControllerButton::Press_Implementation() +{ + this->ControllerButtonState = EControllerButtonState::Pressed; + this->ButtonStyle.Normal = this->ButtonStyle.Pressed; + this->SlateButton->SetPadding(this->ButtonStyle.PressedPadding); + FSlateApplication::Get().PlaySound(this->ButtonStyle.PressedSlateSound); + this->SlateHandlePressed(); +} + +void UControllerButton::Release_Implementation() +{ + this->ControllerButtonState = EControllerButtonState::Hovered; + this->ButtonStyle.Normal = this->ButtonStyle.Hovered; + this->SlateButton->SetPadding(this->ButtonStyle.NormalPadding); + this->SlateHandleReleased(); +} + +void UControllerButton::Hover_Implementation() +{ + this->ControllerButtonState = EControllerButtonState::Hovered; + this->ButtonStyle.Normal = this->ButtonStyle.Hovered; + this->SlateButton->SetPadding(this->ButtonStyle.NormalPadding); + FSlateApplication::Get().PlaySound(this->ButtonStyle.HoveredSlateSound); + this->SlateHandleHovered(); +} + +void UControllerButton::Unhover_Implementation() +{ + this->ControllerButtonState = EControllerButtonState::Normal; + this->ButtonStyle.Normal = this->NormalBrush; + this->SlateButton->SetPadding(this->NormalPadding); + this->SlateHandleUnhovered(); +} +/** END ITakesControllerFocus implementation */ + + +#undef LOCTEXT_NAMESPACE diff --git a/Source/ControllerMenu/Private/Widgets/Components/TextSpinner.cpp b/Source/ControllerMenu/Private/Widgets/Components/TextSpinner.cpp new file mode 100644 index 0000000..716475b --- /dev/null +++ b/Source/ControllerMenu/Private/Widgets/Components/TextSpinner.cpp @@ -0,0 +1,476 @@ +/** + * Copyright ©2019 Batty Bovine Productions, LLC. All Rights Reserved. + **/ + + +#include "TextSpinner.h" + +#include "ConstructorHelpers.h" +#include "SCustomTextBlock.h" + +#include "Engine/Font.h" +#include "Framework/Application/SlateApplication.h" +#include "Input/SButton.h" +#include "Misc/ConfigCacheIni.h" +#include "Widgets/SBoxPanel.h" + + +#define LOCTEXT_NAMESPACE "TextSpinner" + + +UTextSpinner::UTextSpinner(const FObjectInitializer& ObjectInitializer) : + Super(ObjectInitializer) +{ + this->bIsVariable = true; + + this->bLoop = false; + this->bIsPressable = false; + + this->bEmitSelectionChangedEvents = true; + + this->OptionTextShadowOffset = FVector2D(1.0f, 1.0f); + this->OptionTextColorAndOpacity = FLinearColor::White; + this->OptionTextColorAndOpacityHovered = FLinearColor::White; + this->OptionTextColorAndOpacityPressed = FLinearColor::White; + this->OptionTextShadowColorAndOpacity = FLinearColor::Transparent; + + this->ButtonTextColorAndOpacity = FLinearColor::White; + this->ButtonTextShadowColorAndOpacity = FLinearColor::Transparent; + + if (this->DefaultSpinnerButtonStyle == nullptr) + { + // HACK: THIS SHOULD NOT COME FROM CORESTYLE AND SHOULD INSTEAD BE DEFINED BY ENGINE TEXTURES/PROJECT SETTINGS + this->DefaultSpinnerButtonStyle = new FButtonStyle(FCoreStyle::Get().GetWidgetStyle("Button")); + + // Unlink UMG default colors from the editor settings colors. + this->DefaultSpinnerButtonStyle->UnlinkColors(); + } + this->PreviousNextButtonStyle = this->OptionButtonStyle = *this->DefaultSpinnerButtonStyle; + + if (!IsRunningDedicatedServer()) + { + static ConstructorHelpers::FObjectFinder RobotoFontObj(TEXT("/Engine/EngineFonts/Roboto")); + this->OptionTextFont = FSlateFontInfo(RobotoFontObj.Object, 24, FName("Bold")); + this->ButtonTextFont = FSlateFontInfo(RobotoFontObj.Object, 24, FName("Bold")); + } + + this->ClickMethod = EButtonClickMethod::DownAndUp; + this->TouchMethod = EButtonTouchMethod::DownAndUp; + this->PressMethod = EButtonPressMethod::ButtonPress; + + // Retrieve and cache the Previous and Next arrows from the plugin settings + if (GConfig) + { + GConfig->GetString(TEXT("/Script/ControllerMenu.ControllerMenuGlobalSettings"), TEXT("TextSpinnerPreviousText"), this->PreviousTextString, GGameIni); + GConfig->GetString(TEXT("/Script/ControllerMenu.ControllerMenuGlobalSettings"), TEXT("TextSpinnerNextText"), this->NextTextString, GGameIni); + } +} + +TSharedRef UTextSpinner::RebuildWidget() +{ + // Create the box which will contain our compound widget + this->ParentHBox = SNew(SHorizontalBox); + + // Generate the "Next" button content + this->NextButtonText = SNew(STextBlock); + this->NextButtonText->SetText(this->NextTextString.IsEmpty() ? FText::AsCultureInvariant(">") : FText::FromString(this->NextTextString)); + this->NextButtonText->SetColorAndOpacity(this->ButtonTextColorAndOpacity); + this->NextButtonText->SetFont(this->ButtonTextFont); + this->NextButtonText->SetMargin(this->ButtonTextMargin); + this->NextButtonText->SetJustification(ETextJustify::Center); + this->NextButton = SNew(SButton) + .OnClicked(BIND_UOBJECT_DELEGATE(FOnClicked, SlateHandleClickedNext)) + .OnPressed(BIND_UOBJECT_DELEGATE(FSimpleDelegate, SlateHandlePressedNext)) + .OnReleased(BIND_UOBJECT_DELEGATE(FSimpleDelegate, SlateHandleReleasedNext)) + .OnHovered_UObject(this, &UTextSpinner::SlateHandleHoveredNext) + .OnUnhovered_UObject(this, &UTextSpinner::SlateHandleUnhoveredNext) + .ButtonStyle(&this->PreviousNextButtonStyle) + .ClickMethod(this->ClickMethod) + .TouchMethod(this->TouchMethod) + .PressMethod(this->PressMethod) + .HAlign(EHorizontalAlignment::HAlign_Right) + .VAlign(EVerticalAlignment::VAlign_Center) + .IsFocusable(this->bIsFocusable) + .IsEnabled(this->Items.Num() > 1) + .Content() + [ + this->NextButtonText.ToSharedRef() + ]; + SHorizontalBox::FScopedWidgetSlotArguments NextButtonSlot = this->ParentHBox->AddSlot(); + NextButtonSlot.AutoWidth(); + NextButtonSlot.HAlign(EHorizontalAlignment::HAlign_Right); + NextButtonSlot.VAlign(EVerticalAlignment::VAlign_Fill); + NextButtonSlot.AttachWidget(this->NextButton.ToSharedRef()); + + this->OptionText = SNew(SCustomTextBlock); + this->OptionText->SetText(this->GetSelectedItem()); + this->OptionText->SetColorAndOpacity(this->OptionTextColorAndOpacity); + this->OptionText->SetFont(this->OptionTextFont); + this->OptionText->SetMargin(this->OptionTextMargin); + this->OptionText->SetJustification(ETextJustify::Center); + this->OptionText->SetPreventDisable(this->bIsPressable); + this->OptionButton = SNew(SButton) + .OnClicked(BIND_UOBJECT_DELEGATE(FOnClicked, SlateHandleClicked)) + .OnPressed(BIND_UOBJECT_DELEGATE(FSimpleDelegate, SlateHandlePressed)) + .OnReleased(BIND_UOBJECT_DELEGATE(FSimpleDelegate, SlateHandleReleased)) + .OnHovered_UObject(this, &UTextSpinner::SlateHandleHovered) + .OnUnhovered_UObject(this, &UTextSpinner::SlateHandleUnhovered) + .ButtonStyle(&this->OptionButtonStyle) + .ClickMethod(this->ClickMethod) + .TouchMethod(this->TouchMethod) + .PressMethod(this->PressMethod) + .HAlign(EHorizontalAlignment::HAlign_Fill) + .VAlign(EVerticalAlignment::VAlign_Center) + .IsFocusable(this->bIsFocusable) + .IsEnabled(this->GetIsEnabled() && this->bIsPressable) + .Content() + [ + this->OptionText.ToSharedRef() + ]; + SHorizontalBox::FScopedWidgetSlotArguments TextSlot = this->ParentHBox->AddSlot(); + TextSlot.FillWidth(1.0); + TextSlot.HAlign(EHorizontalAlignment::HAlign_Fill); + TextSlot.VAlign(EVerticalAlignment::VAlign_Fill); + TextSlot.AttachWidget(this->OptionButton.ToSharedRef()); + + // Generate the "Previous" button content + this->PreviousButtonText = SNew(STextBlock); + this->PreviousButtonText->SetText(this->PreviousTextString.IsEmpty() ? FText::AsCultureInvariant("<") : FText::FromString(this->PreviousTextString)); + this->PreviousButtonText->SetColorAndOpacity(this->ButtonTextColorAndOpacity); + this->PreviousButtonText->SetFont(this->ButtonTextFont); + this->PreviousButtonText->SetMargin(this->ButtonTextMargin); + this->PreviousButtonText->SetJustification(ETextJustify::Center); + this->PreviousButton = SNew(SButton) + .OnClicked(BIND_UOBJECT_DELEGATE(FOnClicked, SlateHandleClickedPrevious)) + .OnPressed(BIND_UOBJECT_DELEGATE(FSimpleDelegate, SlateHandlePressedPrevious)) + .OnReleased(BIND_UOBJECT_DELEGATE(FSimpleDelegate, SlateHandleReleasedPrevious)) + .OnHovered_UObject(this, &UTextSpinner::SlateHandleHoveredPrevious) + .OnUnhovered_UObject(this, &UTextSpinner::SlateHandleUnhoveredPrevious) + .ButtonStyle(&this->PreviousNextButtonStyle) + .ClickMethod(this->ClickMethod) + .TouchMethod(this->TouchMethod) + .PressMethod(this->PressMethod) + .HAlign(EHorizontalAlignment::HAlign_Left) + .VAlign(EVerticalAlignment::VAlign_Center) + .IsFocusable(this->bIsFocusable) + .IsEnabled(this->Items.Num() > 1) + .Content() + [ + this->PreviousButtonText.ToSharedRef() + ]; + SHorizontalBox::FScopedWidgetSlotArguments PreviousButtonSlot = this->ParentHBox->AddSlot(); + PreviousButtonSlot.AutoWidth(); + PreviousButtonSlot.HAlign(EHorizontalAlignment::HAlign_Left); + PreviousButtonSlot.VAlign(EVerticalAlignment::VAlign_Fill); + PreviousButtonSlot.AttachWidget(this->PreviousButton.ToSharedRef()); + + // Cache our normal style settings for programmatically changing the style later + this->NormalBrush = this->OptionButtonStyle.Normal; + this->NormalPadding = this->OptionButtonStyle.NormalPadding; + + return this->ParentHBox.ToSharedRef(); +} + + +void UTextSpinner::SetSelectedItem(const FText Item/*, ESelectInfo::Type SelectionType*/) +{ + const int32 FoundIndex = this->FindItemIndex(Item); + if (FoundIndex != INDEX_NONE) + { + this->SetSelectedIndex(FoundIndex); + if (this->bEmitSelectionChangedEvents) + { + this->OnSelectionChanged.Broadcast(this->Items[this->SelectedItemIndex], this->SelectedItemIndex); + } + } +} + +bool UTextSpinner::SetSelectedIndex(const int32 Index/*, ESelectInfo::Type SelectionType*/) +{ + if (const int32 NumItems = this->Items.Num()) + { + int32 NewSelection = INDEX_NONE; + if (this->bLoop) + { + NewSelection = (Index + NumItems) % NumItems; + } + else + { + NewSelection = FMath::Clamp(Index, 0, NumItems - 1); + } + + if (NewSelection > INDEX_NONE && NewSelection != this->SelectedItemIndex) + { + this->SelectedItemIndex = NewSelection; + this->OptionText->SetText(this->Items[this->SelectedItemIndex]); + if (this->bEmitSelectionChangedEvents) + { + this->OnSelectionChanged.Broadcast(this->Items[this->SelectedItemIndex], this->SelectedItemIndex); + } + return true; + } + } + else + { + this->SelectedItemIndex = INDEX_NONE; + } + + return false; +} + +void UTextSpinner::AddItem(const FText Item) +{ + this->Items.Emplace(Item); + + if (this->SelectedItemIndex < 0) + { + this->SelectedItemIndex = 0; + } + + if (this->Items.Num() > 1) + { + this->PreviousButton->SetEnabled(true); + this->NextButton->SetEnabled(true); + } + else + { + this->PreviousButton->SetEnabled(false); + this->NextButton->SetEnabled(false); + } +} + +void UTextSpinner::PreviousItem() +{ + if (this->PreviousButton->IsEnabled()) + { + if (this->SetSelectedIndex(this->SelectedItemIndex - 1)) + { + FSlateApplication::Get().PlaySound(this->PreviousNextButtonStyle.PressedSlateSound); + } + } +} + +void UTextSpinner::NextItem() +{ + if (this->NextButton->IsEnabled()) + { + if (this->SetSelectedIndex(this->SelectedItemIndex + 1)) + { + FSlateApplication::Get().PlaySound(this->PreviousNextButtonStyle.PressedSlateSound); + } + } +} + +int32 UTextSpinner::FindItemIndex(const FText& Item) const +{ + if (this->Items.Num()) + { + int32 FoundIndex; + for (FoundIndex = 0; FoundIndex < this->Items.Num(); FoundIndex++) + { + if (Item.EqualTo(this->Items[FoundIndex])) + { + return FoundIndex; + } + } + } + return INDEX_NONE; +} + + +void UTextSpinner::SynchronizeProperties() +{ + Super::SynchronizeProperties(); + + this->PreviousButtonText->SetFont(this->ButtonTextFont); + this->PreviousButtonText->SetColorAndOpacity(this->ButtonTextColorAndOpacity); + this->PreviousButtonText->SetMargin(this->ButtonTextMargin); + this->PreviousButtonText->SetJustification(ETextJustify::Center); + + this->OptionText->SetText(this->GetSelectedItem()); + this->OptionText->SetFont(this->OptionTextFont); + switch (this->OptionButtonState) + { + case ETextSpinnerState::Normal: + this->OptionText->SetColorAndOpacity(this->OptionTextColorAndOpacity); + break; + case ETextSpinnerState::Hovered: + this->OptionText->SetColorAndOpacity(this->OptionTextColorAndOpacityHovered); + break; + case ETextSpinnerState::Pressed: + if (this->bIsPressable) + { + this->OptionText->SetColorAndOpacity(this->OptionTextColorAndOpacityPressed); + } + break; + } + this->OptionText->SetMargin(this->OptionTextMargin); + this->OptionText->SetJustification(this->OptionTextJustification); + + this->NextButtonText->SetFont(this->ButtonTextFont); + this->NextButtonText->SetColorAndOpacity(this->ButtonTextColorAndOpacity); + this->NextButtonText->SetMargin(this->ButtonTextMargin); + this->NextButtonText->SetJustification(ETextJustify::Center); +} + +void UTextSpinner::ReleaseSlateResources(bool bReleaseChildren) +{ + Super::ReleaseSlateResources(bReleaseChildren); + + this->PreviousButton.Reset(); + this->PreviousButtonText.Reset(); + this->OptionButton.Reset(); + this->OptionText.Reset(); + this->NextButton.Reset(); + this->NextButtonText.Reset(); + this->ParentHBox.Reset(); +} + + +FReply UTextSpinner::SlateHandleClicked() +{ + if (this->bIsPressable) + { + this->OnClicked.Broadcast(); + + return FReply::Handled(); + } + + return FReply::Unhandled(); +} + +void UTextSpinner::SlateHandlePressed() +{ + if (this->bIsPressable) + { + this->OnPressed.Broadcast(); + } +} + +void UTextSpinner::SlateHandleReleased() +{ + if (this->bIsPressable) + { + this->OnReleased.Broadcast(); + } +} + +void UTextSpinner::SlateHandleHovered() +{ + this->OnHovered.Broadcast(); +} + +void UTextSpinner::SlateHandleUnhovered() +{ + this->OnUnhovered.Broadcast(); +} + + +FReply UTextSpinner::SlateHandleClickedPrevious() +{ + this->OnPreviousClicked.Broadcast(); + + return FReply::Handled(); +} + +void UTextSpinner::SlateHandlePressedPrevious() +{ + this->SetSelectedIndex(this->SelectedItemIndex - 1); + + this->OnPreviousPressed.Broadcast(); +} + +void UTextSpinner::SlateHandleReleasedPrevious() +{ + this->OnPreviousReleased.Broadcast(); +} + +void UTextSpinner::SlateHandleHoveredPrevious() +{ + this->OnPreviousHovered.Broadcast(); +} + +void UTextSpinner::SlateHandleUnhoveredPrevious() +{ + this->OnPreviousUnhovered.Broadcast(); +} + + +FReply UTextSpinner::SlateHandleClickedNext() +{ + this->OnNextClicked.Broadcast(); + + return FReply::Handled(); +} + +void UTextSpinner::SlateHandlePressedNext() +{ + this->SetSelectedIndex(this->SelectedItemIndex + 1); + + this->OnNextPressed.Broadcast(); +} + +void UTextSpinner::SlateHandleReleasedNext() +{ + this->OnNextReleased.Broadcast(); +} + +void UTextSpinner::SlateHandleHoveredNext() +{ + this->OnNextHovered.Broadcast(); +} + +void UTextSpinner::SlateHandleUnhoveredNext() +{ + this->OnNextUnhovered.Broadcast(); +} + + +/** BEGIN ITakesControllerFocus implementation */ +void UTextSpinner::Press_Implementation() +{ + if (this->bIsPressable) + { + this->OptionButtonState = ETextSpinnerState::Pressed; + this->OptionButtonStyle.Normal = this->OptionButtonStyle.Pressed; + this->OptionButton->SetPadding(this->OptionButtonStyle.PressedPadding); + FSlateApplication::Get().PlaySound(this->OptionButtonStyle.PressedSlateSound); + this->OptionText->SetColorAndOpacity(this->OptionTextColorAndOpacityPressed); + this->SlateHandlePressed(); + } +} + +void UTextSpinner::Release_Implementation() +{ + if (this->bIsPressable) + { + this->OptionButtonState = ETextSpinnerState::Hovered; + this->OptionButtonStyle.Normal = this->OptionButtonStyle.Hovered; + this->OptionButton->SetPadding(this->OptionButtonStyle.NormalPadding); + this->OptionText->SetColorAndOpacity(this->OptionTextColorAndOpacityHovered); + this->SlateHandleReleased(); + } +} + +void UTextSpinner::Hover_Implementation() +{ + this->OptionButtonState = ETextSpinnerState::Hovered; + this->OptionButtonStyle.Normal = this->OptionButtonStyle.Hovered; + this->OptionButton->SetPadding(this->OptionButtonStyle.NormalPadding); + FSlateApplication::Get().PlaySound(this->OptionButtonStyle.HoveredSlateSound); + this->OptionText->SetColorAndOpacity(this->OptionTextColorAndOpacityHovered); + this->SlateHandleHovered(); +} + +void UTextSpinner::Unhover_Implementation() +{ + this->OptionButtonState = ETextSpinnerState::Normal; + this->OptionButtonStyle.Normal = this->NormalBrush; + this->OptionButton->SetPadding(this->OptionButtonStyle.NormalPadding); + this->OptionText->SetColorAndOpacity(this->OptionTextColorAndOpacity); + this->SlateHandleUnhovered(); +} +/** END ITakesControllerFocus implementation */ + + +#undef LOCTEXT_NAMESPACE diff --git a/Source/ControllerMenu/Private/Widgets/ControllerMenuMessageBoxWidget.cpp b/Source/ControllerMenu/Private/Widgets/ControllerMenuMessageBoxWidget.cpp new file mode 100644 index 0000000..40f9c79 --- /dev/null +++ b/Source/ControllerMenu/Private/Widgets/ControllerMenuMessageBoxWidget.cpp @@ -0,0 +1,58 @@ +/** + * Copyright ©2019 Batty Bovine Productions, LLC. All Rights Reserved. + **/ + + +#include "ControllerMenuMessageBoxWidget.h" + +#include "Components/ControllerButton.h" +#include "Components/TextBlock.h" + + +bool UControllerMenuMessageBoxWidget::Initialize() +{ + if (Super::Initialize() && (this->AcceptButton && this->CancelButton)) + { + this->AddHorizontalFocusableWidget(this->CancelButton); + this->AddHorizontalFocusableWidget(this->AcceptButton); + this->SetCancelButton(this->CancelButton); + + this->AcceptButton->OnPressed.AddDynamic(this, &UControllerMenuMessageBoxWidget::Accepted); + this->CancelButton->OnPressed.AddDynamic(this, &UControllerMenuMessageBoxWidget::Cancelled); + + return true; + } + + return false; +} + + +void UControllerMenuMessageBoxWidget::SetMessageBody(const FText& Message) +{ + this->MessageBody->SetText(Message); +} + +void UControllerMenuMessageBoxWidget::SetAcceptText(const FText& Accept) +{ + this->AcceptText->SetText(Accept); +} + +void UControllerMenuMessageBoxWidget::SetCancelText(const FText& Cancel) +{ + this->CancelText->SetText(Cancel); +} + + +void UControllerMenuMessageBoxWidget::Accepted() +{ + this->UnbindMenuActions(); + this->OnAccepted.Broadcast(); + this->ExitMenu(); +} + +void UControllerMenuMessageBoxWidget::Cancelled() +{ + this->UnbindMenuActions(); + this->OnCancelled.Broadcast(); + this->ExitMenu(); +} diff --git a/Source/ControllerMenu/Private/Widgets/ControllerMenuWidget.cpp b/Source/ControllerMenu/Private/Widgets/ControllerMenuWidget.cpp new file mode 100644 index 0000000..09ffcd7 --- /dev/null +++ b/Source/ControllerMenu/Private/Widgets/ControllerMenuWidget.cpp @@ -0,0 +1,1408 @@ +/** + * Copyright ©2019 Batty Bovine Productions, LLC. All Rights Reserved. + **/ + +#include "ControllerMenuWidget.h" + +#include "ConstructorHelpers.h" +#include "ContentWidget.h" +#include "IControllerMenu_Implementation.h" +#include "PanelWidget.h" + +#include "Blueprint/WidgetTree.h" +#include "Components/Button.h" +#include "Components/ComboBox.h" +#include "Components/GridPanel.h" +#include "Components/GridSlot.h" +#include "Components/ScrollBox.h" +#include "Components/TextSpinner.h" +#include "GameFramework/GameModeBase.h" +#include "GameFramework/PlayerInput.h" +#include "Kismet/GameplayStatics.h" +#include "Misc/ConfigCacheIni.h" +#include "Widgets/ControllerMenuMessageBoxWidget.h" + + +UControllerMenuWidget::UControllerMenuWidget(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ + this->bIsFocusable = false; + this->bIsChildMenu = false; + + if (GConfig) + { + GConfig->GetFloat(TEXT("/Script/ControllerMenu.ControllerMenuGlobalSettings"), TEXT("AxisMovementThreshold"), this->AxisMovementThreshold, GGameIni); + GConfig->GetFloat(TEXT("/Script/ControllerMenu.ControllerMenuGlobalSettings"), TEXT("MouseMovementThreshold"), this->MouseMovementThreshold, GGameIni); + GConfig->GetBool(TEXT("/Script/ControllerMenu.ControllerMenuGlobalSettings"), TEXT("CancelConfirm"), this->CancelConfirm, GGameIni); + } + + this->bAxisYHasMoved = this->bAxisXHasMoved = false; + + //this->InputComponent = CreateDefaultSubobject(TEXT("MenuInput")); + //this->InputComponent->RegisterComponent(); +} + + +void UControllerMenuWidget::NativeOnInitialized() +{ + Super::NativeOnInitialized(); + + // HACK: Enable focus for this widget, focus, then reset focus setting. + // This allows us to keep focus inside the widget while preventing Slate + // from using its own input handling, and also bypasses a warning about + // setting focus to an unfocusable widget. + this->bIsFocusable = true; + this->SetKeyboardFocus(); + this->bIsFocusable = false; + + // Bind both the close and open animation events now + if (this->OpenAnimation) + { + FWidgetAnimationDynamicEvent OpenEvent; + OpenEvent.BindDynamic(this, &UControllerMenuWidget::OpenAnimationFinished); + this->BindToAnimationFinished(this->OpenAnimation, OpenEvent); + } + if (this->CloseAnimation) + { + FWidgetAnimationDynamicEvent CloseEvent; + CloseEvent.BindDynamic(this, &UControllerMenuWidget::CloseAnimationFinished); + this->BindToAnimationFinished(this->CloseAnimation, CloseEvent); + } + + this->InitializeMenuActions(); +} + +FReply UControllerMenuWidget::NativeOnKeyDown(const FGeometry& InGeometry, const FKeyEvent& InKeyEvent) +{ + if (InKeyEvent.GetKey().GetDisplayName().ToString().StartsWith("Gamepad")) + { + if (this->LastInputDevice != EControllerMenuInputDevice::Controller) + { + this->SetNewInputDevice(EControllerMenuInputDevice::Controller); + } + } + else + { + if (this->LastInputDevice != EControllerMenuInputDevice::Keyboard) + { + this->SetNewInputDevice(EControllerMenuInputDevice::Keyboard); + } + } + + //FReply Reply = Super::NativeOnKeyDown(InGeometry, InKeyEvent); + //this->ResetUserFocus(); + //return Reply; + return Super::NativeOnKeyDown(InGeometry, InKeyEvent); +} + +FReply UControllerMenuWidget::NativeOnKeyUp(const FGeometry& InGeometry, const FKeyEvent& InKeyEvent) +{ + if (InKeyEvent.GetKey().GetDisplayName().ToString().StartsWith("Gamepad")) + { + if (this->LastInputDevice != EControllerMenuInputDevice::Controller) + { + this->SetNewInputDevice(EControllerMenuInputDevice::Controller); + } + } + else + { + if (this->LastInputDevice != EControllerMenuInputDevice::Keyboard) + { + this->SetNewInputDevice(EControllerMenuInputDevice::Keyboard); + } + } + + //FReply Reply = Super::NativeOnKeyUp(InGeometry, InKeyEvent); + //this->ResetUserFocus(); + //return Reply; + return Super::NativeOnKeyUp(InGeometry, InKeyEvent); +} + +FReply UControllerMenuWidget::NativeOnMouseMove(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent) +{ + if (this->LastInputDevice != EControllerMenuInputDevice::Mouse) + { + if (this->MouseMovementStartPosition == FVector2D::ZeroVector) + { + this->MouseMovementStartPosition = InMouseEvent.GetLastScreenSpacePosition(); + } + const float MouseMovementDistance = (InMouseEvent.GetScreenSpacePosition() - this->MouseMovementStartPosition).Size(); + if (MouseMovementDistance >= this->MouseMovementThreshold) + { + this->ClearWidgetFocus(); + this->SetNewInputDevice(EControllerMenuInputDevice::Mouse); + } + } + + return Super::NativeOnMouseMove(InGeometry, InMouseEvent); +} + +FReply UControllerMenuWidget::NativeOnMouseButtonDown(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent) +{ + if (this->LastInputDevice != EControllerMenuInputDevice::Mouse) + { + this->SetNewInputDevice(EControllerMenuInputDevice::Mouse); + } + + //FReply Reply = Super::NativeOnMouseMove(InGeometry, InMouseEvent); + //this->ResetUserFocus(); + //return Reply; + return Super::NativeOnMouseButtonDown(InGeometry, InMouseEvent); +} + +FReply UControllerMenuWidget::NativeOnMouseButtonUp(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent) +{ + if (this->LastInputDevice != EControllerMenuInputDevice::Mouse) + { + this->SetNewInputDevice(EControllerMenuInputDevice::Mouse); + } + + //FReply Reply = Super::NativeOnMouseMove(InGeometry, InMouseEvent); + //this->ResetUserFocus(); + //return Reply; + return Super::NativeOnMouseButtonUp(InGeometry, InMouseEvent); +} + + +void UControllerMenuWidget::OpenMenu_Implementation(int32 Depth) +{ + if (!this->IsInViewport()) + { + this->AddToViewport(Depth); + this->WidgetDepth = Depth; + } + + this->PlayAnimation(this->OpenAnimation); + + // Enable menu actions after menu is opened + this->BindMenuActions(); + + this->OnOpenOverride(); + + this->MenuOpened(); + this->OnMenuOpened.Broadcast(); +} + +void UControllerMenuWidget::OpenAnimationFinished() +{ + this->bIsInteractable = true; + if (this->FocusableWidgetArray.Num() > 0) + { + this->SetWidgetFocus(this->FocusableWidgetArray[0]); + } + + this->MenuShown(); + this->OnMenuShown.Broadcast(); +} + +void UControllerMenuWidget::OpenChildMenu(UControllerMenuWidget *Child, bool TransitionOut) +{ + if (Child) + { + this->UnbindMenuActions(); + + this->ChildMenu = Child; + this->ChildMenu->bIsChildMenu = true; + this->ChildMenu->OpenMenu(this->WidgetDepth+1); + this->ChildMenu->ChildMenuExitedDelegate.BindUObject(this, &UControllerMenuWidget::ChildMenuExitedEvent); + this->ChildMenu->ChildMenuClosedDelegate.BindUObject(this, &UControllerMenuWidget::ChildMenuClosedEvent); + this->ChildMenuOpened(); + this->bIsInteractable = false; + } +} + +void UControllerMenuWidget::ChildMenuExitedEvent() +{ + // Switch the pressed button back to the hovered style when coming back to this menu + this->SetWidgetFocus(this->GetFocusedWidget()); + + this->ChildMenuExited(); +} + +void UControllerMenuWidget::ChildMenuClosedEvent() +{ + this->bIsInteractable = true; + this->ChildMenu->RemoveFromParent(); + this->ChildMenu = nullptr; + + this->BindMenuActions(); + + this->ChildMenuClosed(); +} + +void UControllerMenuWidget::ExitMenu_Implementation() +{ + this->ClearWidgetFocus(); + + this->PlayAnimation(this->CloseAnimation); + + this->OnCloseOverride(); + + this->MenuExited(); + this->OnMenuExited.Broadcast(); + + this->ChildMenuExitedDelegate.ExecuteIfBound(); +} + +void UControllerMenuWidget::CloseAnimationFinished() +{ + // Remove menu actions after closing the menu + this->UnbindMenuActions(); + + this->RemoveFromParent(); + this->bIsInteractable = false; + + this->MenuClosed(); + this->OnMenuClosed.Broadcast(); + + this->ChildMenuClosedDelegate.ExecuteIfBound(); +} + + +UControllerMenuMessageBoxWidget* UControllerMenuWidget::CreateMessageBox(const TSubclassOf Class, const FText Message, const FText AcceptText, const FText CancelText) +{ + this->MessageBox = CreateWidget(this->GetGameInstance(), Class); + if (!this->MessageBox) + { + UE_LOG(ControllerMenuLog, Error, TEXT("Message Box could not be loaded")); + return nullptr; + } + + this->UnbindMenuActions(); + + this->MessageBox->bIsChildMenu = true; + this->MessageBox->SetMessageBody(Message); + this->MessageBox->SetAcceptText(AcceptText); + this->MessageBox->SetCancelText(CancelText); + this->MessageBox->OpenMenu(this->WidgetDepth + 1); + this->MessageBox->ChildMenuExitedDelegate.BindUObject(this, &UControllerMenuWidget::MessageBoxExitedEvent); + this->MessageBox->ChildMenuClosedDelegate.BindUObject(this, &UControllerMenuWidget::MessageBoxClosedEvent); + this->bIsInteractable = false; + this->MessageBoxOpened(); + return this->MessageBox; +} + +void UControllerMenuWidget::MessageBoxExitedEvent() +{ + // Switch the pressed button back to the hovered style when coming back to this menu + this->SetWidgetFocus(this->GetFocusedWidget()); + + this->MessageBoxExited(); +} + +void UControllerMenuWidget::MessageBoxClosedEvent() +{ + this->bIsInteractable = true; + this->MessageBox->RemoveFromParent(); + this->MessageBox = nullptr; + + this->BindMenuActions(); + + this->MessageBoxClosed(); +} + + +void UControllerMenuWidget::AddGridPanelWidgets(UGridPanel* GridPanel) +{ + const TArray Slots = GridPanel->GetSlots(); + + // Find the number of rows and columns in this grid panel + int32 MaxColumn = 0; + int32 MaxRow = 0; + for (UPanelSlot* LocalSlot : Slots) + { + if (UGridSlot* GridSlot = Cast(LocalSlot)) + { + if (GridSlot->GetColumn() > MaxColumn) + { + MaxColumn = GridSlot->GetColumn(); + } + if (GridSlot->GetRow() > MaxRow) + { + MaxRow = GridSlot->GetRow(); + } + } + } + + // Create an array of widgets for us to fill out + UWidget*** OrderedWidgetArray = new UWidget**[MaxRow+1]; + for(int32 i = 0; i <= MaxRow; i++) + { + OrderedWidgetArray[i] = new UWidget*[MaxColumn+1]; + for (int32 j = 0; j <= MaxColumn; j++) + { + OrderedWidgetArray[i][j] = nullptr; + } + } + //TArray< TArray > OrderedWidgetArray; + //OrderedWidgetArray.SetNum(MaxColumn); + //for(int32 i = 0; i < MaxColumn; i++) + //{ + // OrderedWidgetArray[i].SetNumZeroed(MaxRow); + //} + + // Add the widgets to the array in the appropriate slots + const TArray Widgets = GridPanel->GetAllChildren(); + for (UWidget* Widget : Widgets) + { + UGridSlot* GridSlot = Cast(Widget->Slot); + OrderedWidgetArray[GridSlot->GetRow()][GridSlot->GetColumn()] = Widget; + } + + // Finally, add each widget to the focus map + for (int Row = 0; Row <= MaxRow; Row++) + { + UWidget** ColumnArray = OrderedWidgetArray[Row]; + for (int Column = 0; Column <= MaxColumn; Column++) + { + UWidget* Widget = ColumnArray[Column]; + if (Widget) + { + FControllerWidgetFocusMap FocusMap; + { + if (Row > 0) + { + UWidget* Up = OrderedWidgetArray[Row - 1][Column]; + if (Cast(Up) || Cast(Up)) + { + FocusMap.Up = Up; + } + } + + if (Row < MaxRow) + { + UWidget* Down = OrderedWidgetArray[Row + 1][Column]; + if (Cast(Down) || Cast(Down)) + { + FocusMap.Down = Down; + } + } + + if (Column > 0) + { + UWidget* Left = ColumnArray[Column - 1]; + if (Cast(Left) || Cast(Left)) + { + FocusMap.Left = Left; + } + } + + if (Column < MaxColumn) + { + UWidget* Right = ColumnArray[Column + 1]; + if (Cast(Right) || Cast(Right)) + { + FocusMap.Right = Right; + } + } + + if ((((MaxRow+1) * Column) + Row) > 0) + { + if (Row > 0) + { + UWidget* Previous = OrderedWidgetArray[Row - 1][Column]; + if (Cast(Previous) || Cast(Previous)) + { + FocusMap.Previous = Previous; + } + } + else + { + UWidget* Previous = OrderedWidgetArray[Row][Column - 1]; + if (Cast(Previous) || Cast(Previous)) + { + FocusMap.Previous = Previous; + } + } + } + else + { + UWidget* Previous = OrderedWidgetArray[MaxRow][MaxColumn]; + if (Cast(Previous) || Cast(Previous)) + { + FocusMap.Previous = Previous; + } + } + + if ((((MaxRow+1) * Column) + Row) < ((MaxRow+1) * (MaxColumn+1)) - 1) + { + if (Row < MaxRow) + { + UWidget* Next = OrderedWidgetArray[Row + 1][Column]; + if (Cast(Next) || Cast(Next)) + { + FocusMap.Next = Next; + } + } + else + { + UWidget* Next = OrderedWidgetArray[0][Column + 1]; + if (Cast(Next) || Cast(Next)) + { + FocusMap.Next = Next; + } + } + } + else + { + UWidget* Next = OrderedWidgetArray[0][0]; + if (Cast(Next) || Cast(Next)) + { + FocusMap.Next = Next; + } + } + } + + if (ITakesControllerFocus* ControllerWidget = Cast(Widget)) + { + this->FocusableWidgetArray.Add(Widget); + this->WidgetFocusMapArray.Add(FocusMap); + this->NormalStyles.Emplace(Widget, ControllerWidget->Execute_GetNormalStyle(Widget)); + this->HoveredStyles.Emplace(Widget, ControllerWidget->Execute_GetHoveredStyle(Widget)); + this->PressedStyles.Emplace(Widget, ControllerWidget->Execute_GetPressedStyle(Widget)); + this->NormalPaddings.Emplace(Widget, ControllerWidget->Execute_GetNormalPadding(Widget)); + this->PressedPaddings.Emplace(Widget, ControllerWidget->Execute_GetPressedPadding(Widget)); + ControllerWidget->Execute_SetFocusable(Widget, false); + } + else if (UButton* Button = Cast(Widget)) + { + this->FocusableWidgetArray.Add(Widget); + this->WidgetFocusMapArray.Add(FocusMap); + this->NormalStyles.Emplace(Widget, Button->WidgetStyle.Normal); + this->HoveredStyles.Emplace(Widget, Button->WidgetStyle.Hovered); + this->PressedStyles.Emplace(Widget, Button->WidgetStyle.Pressed); + this->NormalPaddings.Emplace(Widget, Button->WidgetStyle.NormalPadding); + this->PressedPaddings.Emplace(Widget, Button->WidgetStyle.PressedPadding); + Button->IsFocusable = false; + } + } + } + } +} + +void UControllerMenuWidget::AddFocusableWidgetCustom(UWidget* Widget, UWidget* Up, UWidget* Down, UWidget* Left, UWidget* Right, UWidget* Previous, UWidget* Next) +{ + this->FocusableWidgetArray.Add(Widget); + + FControllerWidgetFocusMap WidgetFocusMap; + WidgetFocusMap.Up = Up; + WidgetFocusMap.Down = Down; + WidgetFocusMap.Left = Left; + WidgetFocusMap.Right = Right; + WidgetFocusMap.Previous = Previous; + WidgetFocusMap.Next = Next; + this->WidgetFocusMapArray.Add(WidgetFocusMap); + + if (ITakesControllerFocus* ControllerWidget = Cast(Widget)) + { + this->NormalStyles.Emplace(Widget, ControllerWidget->Execute_GetNormalStyle(Widget)); + this->HoveredStyles.Emplace(Widget, ControllerWidget->Execute_GetHoveredStyle(Widget)); + this->PressedStyles.Emplace(Widget, ControllerWidget->Execute_GetPressedStyle(Widget)); + this->NormalPaddings.Emplace(Widget, ControllerWidget->Execute_GetNormalPadding(Widget)); + this->PressedPaddings.Emplace(Widget, ControllerWidget->Execute_GetPressedPadding(Widget)); + ControllerWidget->Execute_SetFocusable(Widget, false); + } + else if (UButton* Button = Cast(Widget)) + { + this->NormalStyles.Emplace(Widget, Button->WidgetStyle.Normal); + this->HoveredStyles.Emplace(Widget, Button->WidgetStyle.Hovered); + this->PressedStyles.Emplace(Widget, Button->WidgetStyle.Pressed); + this->NormalPaddings.Emplace(Widget, Button->WidgetStyle.NormalPadding); + this->PressedPaddings.Emplace(Widget, Button->WidgetStyle.PressedPadding); + Button->IsFocusable = false; + } +} + +void UControllerMenuWidget::AddVerticalFocusableWidget(UWidget* Widget, UWidget* Left, UWidget* Right) +{ + FControllerWidgetFocusMap WidgetFocusMap; + const int32 ArraySize = this->FocusableWidgetArray.Num(); + if (ArraySize > 0) + { + WidgetFocusMap.Up = WidgetFocusMap.Previous = this->FocusableWidgetArray[ArraySize - 1]; + WidgetFocusMap.Down = WidgetFocusMap.Next = this->FocusableWidgetArray[0]; + this->WidgetFocusMapArray[0].Up = this->WidgetFocusMapArray[0].Previous = Widget; + this->WidgetFocusMapArray[ArraySize - 1].Down = this->WidgetFocusMapArray[ArraySize - 1].Next = Widget; + } + WidgetFocusMap.Left = Left; + WidgetFocusMap.Right = Right; + this->FocusableWidgetArray.Add(Widget); + this->WidgetFocusMapArray.Add(WidgetFocusMap); + + if (ITakesControllerFocus* ControllerWidget = Cast(Widget)) + { + this->NormalStyles.Emplace(Widget, ControllerWidget->Execute_GetNormalStyle(Widget)); + this->HoveredStyles.Emplace(Widget, ControllerWidget->Execute_GetHoveredStyle(Widget)); + this->PressedStyles.Emplace(Widget, ControllerWidget->Execute_GetPressedStyle(Widget)); + this->NormalPaddings.Emplace(Widget, ControllerWidget->Execute_GetNormalPadding(Widget)); + this->PressedPaddings.Emplace(Widget, ControllerWidget->Execute_GetPressedPadding(Widget)); + ControllerWidget->Execute_SetFocusable(Widget, false); + } + else if (UButton* Button = Cast(Widget)) + { + this->NormalStyles.Emplace(Widget, Button->WidgetStyle.Normal); + this->HoveredStyles.Emplace(Widget, Button->WidgetStyle.Hovered); + this->PressedStyles.Emplace(Widget, Button->WidgetStyle.Pressed); + this->NormalPaddings.Emplace(Widget, Button->WidgetStyle.NormalPadding); + this->PressedPaddings.Emplace(Widget, Button->WidgetStyle.PressedPadding); + Button->IsFocusable = false; + } +} + +void UControllerMenuWidget::AddHorizontalFocusableWidget(UWidget* Widget, UWidget* Up, UWidget* Down) +{ + FControllerWidgetFocusMap WidgetFocusMap; + const int32 ArraySize = this->FocusableWidgetArray.Num(); + if (ArraySize > 0) + { + WidgetFocusMap.Left = WidgetFocusMap.Previous = this->FocusableWidgetArray[ArraySize - 1]; + WidgetFocusMap.Right = WidgetFocusMap.Next = this->FocusableWidgetArray[0]; + this->WidgetFocusMapArray[0].Left = this->WidgetFocusMapArray[0].Previous = Widget; + this->WidgetFocusMapArray[ArraySize - 1].Right = this->WidgetFocusMapArray[ArraySize - 1].Next = Widget; + } + WidgetFocusMap.Up = Up; + WidgetFocusMap.Down = Down; + this->FocusableWidgetArray.Add(Widget); + this->WidgetFocusMapArray.Add(WidgetFocusMap); + + if (ITakesControllerFocus* ControllerWidget = Cast(Widget)) + { + this->NormalStyles.Emplace(Widget, ControllerWidget->Execute_GetNormalStyle(Widget)); + this->HoveredStyles.Emplace(Widget, ControllerWidget->Execute_GetHoveredStyle(Widget)); + this->PressedStyles.Emplace(Widget, ControllerWidget->Execute_GetPressedStyle(Widget)); + this->NormalPaddings.Emplace(Widget, ControllerWidget->Execute_GetNormalPadding(Widget)); + this->PressedPaddings.Emplace(Widget, ControllerWidget->Execute_GetPressedPadding(Widget)); + ControllerWidget->Execute_SetFocusable(Widget, false); + } + else if (UButton* Button = Cast(Widget)) + { + this->NormalStyles.Emplace(Widget, Button->WidgetStyle.Normal); + this->HoveredStyles.Emplace(Widget, Button->WidgetStyle.Hovered); + this->PressedStyles.Emplace(Widget, Button->WidgetStyle.Pressed); + this->NormalPaddings.Emplace(Widget, Button->WidgetStyle.NormalPadding); + this->PressedPaddings.Emplace(Widget, Button->WidgetStyle.PressedPadding); + Button->IsFocusable = false; + } +} + +void UControllerMenuWidget::EditWidgetFocus(UWidget* Widget, UWidget* Up, UWidget* Down, UWidget* Left, UWidget* Right, UWidget* Previous, UWidget* Next) +{ + const int32 WidgetIndex = this->FocusableWidgetArray.Find(Widget); + if (WidgetIndex != INDEX_NONE) + { + FControllerWidgetFocusMap& Map = this->WidgetFocusMapArray[WidgetIndex]; + Map.Up = Up ? Up : Map.Up; + Map.Down = Down ? Down : Map.Down; + Map.Left = Left ? Left : Map.Left; + Map.Right = Right ? Right : Map.Right; + Map.Previous = Previous ? Previous : Map.Previous; + Map.Next = Next ? Next : Map.Next; + } +} + +void UControllerMenuWidget::SetCancelButton(UWidget* Button) +{ + this->CancelButton = Button; +} + + +void UControllerMenuWidget::InitializeMenuActions() +{ + if (GConfig) + { + FString AxisYAction; + GConfig->GetString(TEXT("/Script/ControllerMenu.ControllerMenuGlobalSettings"), TEXT("AxisYAction"), AxisYAction, GGameIni); + if (AxisYAction.IsEmpty()) + { + FString UpAction; + GConfig->GetString(TEXT("/Script/ControllerMenu.ControllerMenuGlobalSettings"), TEXT("UpAction"), UpAction, GGameIni); + this->UpActionBinding = FInputActionBinding(FName(*UpAction), EInputEvent::IE_Pressed); + this->UpActionBinding.bExecuteWhenPaused = true; + this->UpActionBinding.bConsumeInput = true; + this->UpActionBinding.ActionDelegate.BindDelegate(this, &UControllerMenuWidget::OnUpPressedEvent); + + FString DownAction; + GConfig->GetString(TEXT("/Script/ControllerMenu.ControllerMenuGlobalSettings"), TEXT("DownAction"), DownAction, GGameIni); + this->DownActionBinding = FInputActionBinding(FName(*DownAction), EInputEvent::IE_Pressed); + this->DownActionBinding.bExecuteWhenPaused = true; + this->DownActionBinding.bConsumeInput = true; + this->DownActionBinding.ActionDelegate.BindDelegate(this, &UControllerMenuWidget::OnDownPressedEvent); + } + else + { + this->AxisYBinding = FInputAxisBinding(FName(*AxisYAction)); + this->AxisYBinding.bExecuteWhenPaused = true; + this->AxisYBinding.bConsumeInput = true; + this->AxisYBinding.AxisDelegate.BindDelegate(this, &UControllerMenuWidget::OnAxisYEvent); + } + + FString AxisXAction; + GConfig->GetString(TEXT("/Script/ControllerMenu.ControllerMenuGlobalSettings"), TEXT("AxisXAction"), AxisXAction, GGameIni); + if (AxisXAction.IsEmpty()) + { + FString LeftAction; + GConfig->GetString(TEXT("/Script/ControllerMenu.ControllerMenuGlobalSettings"), TEXT("LeftAction"), LeftAction, GGameIni); + this->LeftActionBinding = FInputActionBinding(FName(*LeftAction), EInputEvent::IE_Pressed); + this->LeftActionBinding.bExecuteWhenPaused = true; + this->LeftActionBinding.bConsumeInput = true; + this->LeftActionBinding.ActionDelegate.BindDelegate(this, &UControllerMenuWidget::OnLeftPressedEvent); + + FString RightAction; + GConfig->GetString(TEXT("/Script/ControllerMenu.ControllerMenuGlobalSettings"), TEXT("RightAction"), RightAction, GGameIni); + this->RightActionBinding = FInputActionBinding(FName(*RightAction), EInputEvent::IE_Pressed); + this->RightActionBinding.bExecuteWhenPaused = true; + this->RightActionBinding.bConsumeInput = true; + this->RightActionBinding.ActionDelegate.BindDelegate(this, &UControllerMenuWidget::OnRightPressedEvent); + } + else + { + this->AxisXBinding = FInputAxisBinding(FName(*AxisXAction)); + this->AxisXBinding.bExecuteWhenPaused = true; + this->AxisXBinding.bConsumeInput = true; + this->AxisXBinding.AxisDelegate.BindDelegate(this, &UControllerMenuWidget::OnAxisXEvent); + } + + FString PreviousAction; + GConfig->GetString(TEXT("/Script/ControllerMenu.ControllerMenuGlobalSettings"), TEXT("PreviousAction"), PreviousAction, GGameIni); + this->PreviousActionBinding = FInputActionBinding(FName(*PreviousAction), EInputEvent::IE_Pressed); + this->PreviousActionBinding.bExecuteWhenPaused = true; + this->PreviousActionBinding.bConsumeInput = true; + this->PreviousActionBinding.ActionDelegate.BindDelegate(this, &UControllerMenuWidget::OnPreviousPressedEvent); + + FString NextAction; + GConfig->GetString(TEXT("/Script/ControllerMenu.ControllerMenuGlobalSettings"), TEXT("NextAction"), NextAction, GGameIni); + this->NextActionBinding = FInputActionBinding(FName(*NextAction), EInputEvent::IE_Pressed); + this->NextActionBinding.bExecuteWhenPaused = true; + this->NextActionBinding.bConsumeInput = true; + this->NextActionBinding.ActionDelegate.BindDelegate(this, &UControllerMenuWidget::OnNextPressedEvent); + + FString AcceptAction; + GConfig->GetString(TEXT("/Script/ControllerMenu.ControllerMenuGlobalSettings"), TEXT("AcceptAction"), AcceptAction, GGameIni); + const FName AcceptActionName(*AcceptAction); + this->AcceptPressActionBinding = FInputActionBinding(AcceptActionName, EInputEvent::IE_Pressed); + this->AcceptPressActionBinding.bExecuteWhenPaused = true; + this->AcceptPressActionBinding.bConsumeInput = true; + this->AcceptPressActionBinding.ActionDelegate.BindDelegate(this, &UControllerMenuWidget::OnAcceptPressedEvent); + this->AcceptReleaseActionBinding = FInputActionBinding(AcceptActionName, EInputEvent::IE_Released); + this->AcceptReleaseActionBinding.bExecuteWhenPaused = true; + this->AcceptReleaseActionBinding.bConsumeInput = true; + this->AcceptReleaseActionBinding.ActionDelegate.BindDelegate(this, &UControllerMenuWidget::OnAcceptReleasedEvent); + + FString CancelAction; + GConfig->GetString(TEXT("/Script/ControllerMenu.ControllerMenuGlobalSettings"), TEXT("CancelAction"), CancelAction, GGameIni); + const FName CancelActionName(*CancelAction); + this->CancelPressActionBinding = FInputActionBinding(CancelActionName, EInputEvent::IE_Pressed); + this->CancelPressActionBinding.bExecuteWhenPaused = true; + this->CancelPressActionBinding.bConsumeInput = true; + this->CancelPressActionBinding.ActionDelegate.BindDelegate(this, &UControllerMenuWidget::OnCancelPressedEvent); + this->CancelReleaseActionBinding = FInputActionBinding(CancelActionName, EInputEvent::IE_Released); + this->CancelReleaseActionBinding.bExecuteWhenPaused = true; + this->CancelReleaseActionBinding.bConsumeInput = true; + this->CancelReleaseActionBinding.ActionDelegate.BindDelegate(this, &UControllerMenuWidget::OnCancelReleasedEvent); + + FString PauseAction; + GConfig->GetString(TEXT("/Script/ControllerMenu.ControllerMenuGlobalSettings"), TEXT("PauseAction"), PauseAction, GGameIni); + this->PauseActionBinding = FInputActionBinding(FName(*PauseAction), EInputEvent::IE_Pressed); + this->PauseActionBinding.bExecuteWhenPaused = true; + this->PauseActionBinding.bConsumeInput = true; + this->PauseActionBinding.ActionDelegate.BindDelegate(this, &UControllerMenuWidget::OnPausePressedEvent); + + //for (const FName CustomAction : this->CustomActions) + //{ + // this->CustomActionBinding = FInputActionBinding(CustomAction, EInputEvent::IE_Pressed); + // this->CustomActionBinding.bExecuteWhenPaused = true; + // this->CustomActionBinding.bConsumeInput = false; + // this->CustomActionBinding.ActionDelegate.BindDelegate(this, &UControllerMenuWidget::OnCustomPressedEvent, CustomAction); + //} + } +} + +void UControllerMenuWidget::BindMenuActions() +{ + if (!this->InputComponent) + { + this->InitializeInputComponent(); + if (!this->InputComponent) + { + return; + } + } + + if (this->AxisYBinding.AxisName.IsNone()) + { + this->InputComponent->AddActionBinding(this->UpActionBinding); + this->InputComponent->AddActionBinding(this->DownActionBinding); + } + else + { + this->InputComponent->AxisBindings.Emplace(this->AxisYBinding); + } + + if (this->AxisXBinding.AxisName.IsNone()) + { + this->InputComponent->AddActionBinding(this->LeftActionBinding); + this->InputComponent->AddActionBinding(this->RightActionBinding); + } + else + { + this->InputComponent->AxisBindings.Emplace(this->AxisXBinding); + } + + this->InputComponent->AddActionBinding(this->PreviousActionBinding); + this->InputComponent->AddActionBinding(this->NextActionBinding); + this->InputComponent->AddActionBinding(this->AcceptPressActionBinding); + this->InputComponent->AddActionBinding(this->AcceptReleaseActionBinding); + this->InputComponent->AddActionBinding(this->CancelPressActionBinding); + this->InputComponent->AddActionBinding(this->CancelReleaseActionBinding); + this->InputComponent->AddActionBinding(this->PauseActionBinding); + + //for (const FName CustomAction : this->CustomActions) + //{ + // this->InputComponent->AddActionBinding(this->CustomActionBinding); + //} +} + +void UControllerMenuWidget::UnbindMenuActions() +{ + APlayerController* Controller = this->GetOwningPlayer(); + if (Controller && GConfig) + { + FString AxisYAction; + GConfig->GetString(TEXT("/Script/ControllerMenu.ControllerMenuGlobalSettings"), TEXT("AxisYAction"), AxisYAction, GGameIni); + if (AxisYAction.IsEmpty()) + { + FString UpAction; + GConfig->GetString(TEXT("/Script/ControllerMenu.ControllerMenuGlobalSettings"), TEXT("UpAction"), UpAction, GGameIni); + this->InputComponent->RemoveActionBinding(*UpAction, EInputEvent::IE_Pressed); + + FString DownAction; + GConfig->GetString(TEXT("/Script/ControllerMenu.ControllerMenuGlobalSettings"), TEXT("DownAction"), DownAction, GGameIni); + this->InputComponent->RemoveActionBinding(*DownAction, EInputEvent::IE_Pressed); + } + else + { + const int32 LastBinding = this->InputComponent->AxisBindings.Num() - 1; + for (int32 i = LastBinding; i >= 0; i--) + { + FInputAxisBinding &Binding = this->InputComponent->AxisBindings[i]; + if ((Binding.AxisName == this->AxisYBinding.AxisName) && + (Binding.AxisDelegate.GetDelegateForManualSet().GetHandle() == this->AxisYBinding.AxisDelegate.GetDelegateForManualSet().GetHandle())) + { + this->InputComponent->AxisBindings.RemoveAt(i); + } + } + } + + FString AxisXAction; + GConfig->GetString(TEXT("/Script/ControllerMenu.ControllerMenuGlobalSettings"), TEXT("AxisXAction"), AxisXAction, GGameIni); + if (AxisXAction.IsEmpty()) + { + FString LeftAction; + GConfig->GetString(TEXT("/Script/ControllerMenu.ControllerMenuGlobalSettings"), TEXT("LeftAction"), LeftAction, GGameIni); + this->InputComponent->RemoveActionBinding(*LeftAction, EInputEvent::IE_Pressed); + + FString RightAction; + GConfig->GetString(TEXT("/Script/ControllerMenu.ControllerMenuGlobalSettings"), TEXT("RightAction"), RightAction, GGameIni); + this->InputComponent->RemoveActionBinding(*RightAction, EInputEvent::IE_Pressed); + } + else + { + const int32 LastBinding = this->InputComponent->AxisBindings.Num() - 1; + for (int32 i = LastBinding; i >= 0; i--) + { + FInputAxisBinding &Binding = this->InputComponent->AxisBindings[i]; + if ((Binding.AxisName == this->AxisXBinding.AxisName) && + (Binding.AxisDelegate.GetDelegateForManualSet().GetHandle() == this->AxisXBinding.AxisDelegate.GetDelegateForManualSet().GetHandle())) + { + this->InputComponent->AxisBindings.RemoveAt(i); + } + } + } + + FString PreviousAction; + GConfig->GetString(TEXT("/Script/ControllerMenu.ControllerMenuGlobalSettings"), TEXT("PreviousAction"), PreviousAction, GGameIni); + this->InputComponent->RemoveActionBinding(*PreviousAction, EInputEvent::IE_Pressed); + + FString NextAction; + GConfig->GetString(TEXT("/Script/ControllerMenu.ControllerMenuGlobalSettings"), TEXT("NextAction"), NextAction, GGameIni); + this->InputComponent->RemoveActionBinding(*NextAction, EInputEvent::IE_Pressed); + + FString AcceptAction; + GConfig->GetString(TEXT("/Script/ControllerMenu.ControllerMenuGlobalSettings"), TEXT("AcceptAction"), AcceptAction, GGameIni); + this->InputComponent->RemoveActionBinding(*AcceptAction, EInputEvent::IE_Pressed); + this->InputComponent->RemoveActionBinding(*AcceptAction, EInputEvent::IE_Released); + + FString CancelAction; + GConfig->GetString(TEXT("/Script/ControllerMenu.ControllerMenuGlobalSettings"), TEXT("CancelAction"), CancelAction, GGameIni); + this->InputComponent->RemoveActionBinding(*CancelAction, EInputEvent::IE_Pressed); + this->InputComponent->RemoveActionBinding(*CancelAction, EInputEvent::IE_Released); + + FString PauseAction; + GConfig->GetString(TEXT("/Script/ControllerMenu.ControllerMenuGlobalSettings"), TEXT("PauseAction"), PauseAction, GGameIni); + this->InputComponent->RemoveActionBinding(*PauseAction, EInputEvent::IE_Pressed); + + //for (const FName CustomAction : this->CustomActions) + //{ + // this->CustomActionBinding = FInputActionBinding(CustomAction, EInputEvent::IE_Pressed); + // this->CustomActionBinding.bExecuteWhenPaused = true; + // this->CustomActionBinding.bConsumeInput = true; + // this->CustomActionBinding.ActionDelegate.BindDelegate(this, &UControllerMenuWidget::OnCustomPressedEvent, CustomAction); + // this->InputComponent->AddActionBinding(this->CustomActionBinding); + //} + } +} + + +void UControllerMenuWidget::OnAxisYEvent(const float AxisValue) +{ + if (!this->bAxisYHasMoved) + { + if (AxisValue >= this->AxisMovementThreshold) + { + this->OnUpPressedEvent(); + this->bAxisYHasMoved = true; + } + else if (AxisValue <= -this->AxisMovementThreshold) + { + this->OnDownPressedEvent(); + this->bAxisYHasMoved = true; + } + } + else + { + if (FMath::Abs(AxisValue) < this->AxisMovementThreshold) + { + this->bAxisYHasMoved = false; + } + } +} + +void UControllerMenuWidget::OnAxisXEvent(const float AxisValue) +{ + if (!this->bAxisXHasMoved) + { + if (AxisValue >= this->AxisMovementThreshold) + { + this->OnRightPressedEvent(); + this->bAxisXHasMoved = true; + } + else if (AxisValue <= -this->AxisMovementThreshold) + { + this->OnLeftPressedEvent(); + this->bAxisXHasMoved = true; + } + } + else + { + if (FMath::Abs(AxisValue) < this->AxisMovementThreshold) + { + this->bAxisXHasMoved = false; + } + } +} + + +void UControllerMenuWidget::NativeOnUpPressed() +{ + if (this->FocusableWidgetArray.Num() > 0) + { + if (this->FocusedWidgetIndex >= 0 && this->FocusedWidgetIndex < this->FocusableWidgetArray.Num()) + { + this->SetWidgetFocus(this->WidgetFocusMapArray[this->FocusedWidgetIndex].Up); + } + else + { + this->FocusedWidgetIndex = 0; + this->SetWidgetFocus(this->FocusableWidgetArray[this->FocusedWidgetIndex]); + } + } +} + +void UControllerMenuWidget::NativeOnDownPressed() +{ + if (this->FocusableWidgetArray.Num() > 0) + { + if (this->FocusedWidgetIndex >= 0 && this->FocusedWidgetIndex < this->FocusableWidgetArray.Num()) + { + this->SetWidgetFocus(this->WidgetFocusMapArray[this->FocusedWidgetIndex].Down); + } + else + { + this->FocusedWidgetIndex = 0; + this->SetWidgetFocus(this->FocusableWidgetArray[this->FocusedWidgetIndex]); + } + } +} + +void UControllerMenuWidget::NativeOnLeftPressed() +{ + if (this->FocusableWidgetArray.Num() > 0) + { + if (this->FocusedWidgetIndex >= 0 && this->FocusedWidgetIndex < this->FocusableWidgetArray.Num()) + { + this->SetWidgetFocus(this->WidgetFocusMapArray[this->FocusedWidgetIndex].Left); + } + else + { + this->FocusedWidgetIndex = 0; + this->SetWidgetFocus(this->FocusableWidgetArray[this->FocusedWidgetIndex]); + } + } +} + +void UControllerMenuWidget::NativeOnRightPressed() +{ + if (this->FocusableWidgetArray.Num() > 0) + { + if (this->FocusedWidgetIndex >= 0 && this->FocusedWidgetIndex < this->FocusableWidgetArray.Num()) + { + this->SetWidgetFocus(this->WidgetFocusMapArray[this->FocusedWidgetIndex].Right); + } + else + { + this->FocusedWidgetIndex = 0; + this->SetWidgetFocus(this->FocusableWidgetArray[this->FocusedWidgetIndex]); + } + } +} + +void UControllerMenuWidget::NativeOnPreviousPressed() +{ + if (this->FocusableWidgetArray.Num() > 0) + { + if (this->FocusedWidgetIndex >= 0 && this->FocusedWidgetIndex < this->FocusableWidgetArray.Num()) + { + this->SetWidgetFocus(this->WidgetFocusMapArray[this->FocusedWidgetIndex].Previous); + } + else + { + this->FocusedWidgetIndex = 0; + this->SetWidgetFocus(this->FocusableWidgetArray[this->FocusedWidgetIndex]); + } + } +} + +void UControllerMenuWidget::NativeOnNextPressed() +{ + if (this->FocusableWidgetArray.Num() > 0) + { + if (this->FocusedWidgetIndex >= 0 && this->FocusedWidgetIndex < this->FocusableWidgetArray.Num()) + { + this->SetWidgetFocus(this->WidgetFocusMapArray[this->FocusedWidgetIndex].Next); + } + else + { + this->FocusedWidgetIndex = 0; + this->SetWidgetFocus(this->FocusableWidgetArray[this->FocusedWidgetIndex]); + } + } +} + +void UControllerMenuWidget::NativeOnAcceptPressed() +{ + if (UWidget* PressedWidget = this->GetFocusedWidget()) + { + if (ITakesControllerFocus* PressedControllerWidget = Cast(PressedWidget)) + { + PressedControllerWidget->Execute_Press(PressedWidget); + } + else if (UButton* PressedButton = Cast(PressedWidget)) + { + PressedButton->WidgetStyle.Normal = this->PressedStyles[PressedWidget]; + PressedButton->WidgetStyle.NormalPadding = this->PressedPaddings[PressedWidget]; + PressedButton->OnPressed.Broadcast(); + } + this->Refocus(PressedWidget); + } +} + +void UControllerMenuWidget::NativeOnCancelPressed() +{ + if (this->CancelButton) + { + if (this->CancelConfirm && !this->HasWidgetFocus(this->CancelButton)) + { + this->SetWidgetFocus(this->CancelButton); + return; + } + else if (ITakesControllerFocus* PressedControllerWidget = Cast(this->CancelButton)) + { + UWidget* FocusedWidget = this->GetFocusedWidget(); + if (ITakesControllerFocus* FocusedControllerWidget = Cast(FocusedWidget)) + { + FocusedControllerWidget->Execute_Unhover(FocusedWidget); + } + else if (UButton* FocusedButton = Cast(FocusedWidget)) + { + FocusedButton->WidgetStyle.Normal = this->NormalStyles[this->CancelButton]; + FocusedButton->WidgetStyle.NormalPadding = this->NormalPaddings[this->CancelButton]; + } + PressedControllerWidget->Execute_Press(this->CancelButton); + } + else if (UButton* PressedButton = Cast(this->CancelButton)) + { + UWidget* FocusedWidget = this->GetFocusedWidget(); + if (ITakesControllerFocus* FocusedControllerWidget = Cast(FocusedWidget)) + { + FocusedControllerWidget->Execute_Unhover(FocusedWidget); + } + else if (UButton* FocusedButton = Cast(FocusedWidget)) + { + FocusedButton->WidgetStyle.Normal = this->NormalStyles[this->CancelButton]; + FocusedButton->WidgetStyle.NormalPadding = this->NormalPaddings[this->CancelButton]; + } + PressedButton->WidgetStyle.Normal = this->PressedStyles[this->CancelButton]; + PressedButton->WidgetStyle.NormalPadding = this->PressedPaddings[this->CancelButton]; + PressedButton->OnPressed.Broadcast(); + } + this->Refocus(this->CancelButton); + } +} + + +void UControllerMenuWidget::NativeOnAcceptReleased() +{ + if (UWidget* ReleasedWidget = this->GetFocusedWidget()) + { + if (ITakesControllerFocus* ReleasedControllerWidget = Cast(ReleasedWidget)) + { + ReleasedControllerWidget->Execute_Release(ReleasedWidget); + } + else if (UButton* ReleasedButton = Cast(ReleasedWidget)) + { + ReleasedButton->WidgetStyle.Normal = this->HoveredStyles[ReleasedWidget]; + ReleasedButton->WidgetStyle.NormalPadding = this->NormalPaddings[ReleasedWidget]; + ReleasedButton->OnReleased.Broadcast(); + } + this->Refocus(ReleasedWidget); + } +} + +void UControllerMenuWidget::NativeOnCancelReleased() +{ + if (this->CancelButton && this->HasWidgetFocus(this->CancelButton)) + { + if (ITakesControllerFocus* ReleasedControllerWidget = Cast(this->CancelButton)) + { + ReleasedControllerWidget->Execute_Release(this->CancelButton); + } + else if (UButton* ReleasedButton = Cast(this->CancelButton)) + { + ReleasedButton->WidgetStyle.Normal = this->HoveredStyles[this->CancelButton]; + ReleasedButton->WidgetStyle.NormalPadding = this->NormalPaddings[this->CancelButton]; + ReleasedButton->OnReleased.Broadcast(); + } + this->Refocus(this->CancelButton); + } +} + + +void UControllerMenuWidget::OnUpPressedEvent() +{ + if (!this->IsAnyAnimationPlaying() && this->bIsInteractable) + { + //const FControllerWidgetFocusMap &WidgetFocusMap = this->WidgetFocusMapArray[this->FocusedWidgetIndex]; + //if (WidgetFocusMap.Up) + //{ + // this->FocusedWidgetIndex = this->FocusableWidgetArray.Find(WidgetFocusMap.Up); + // this->ResetFocus(); + //} + + this->NativeOnUpPressed(); + this->OnUpPressed(); + this->OnUpPressedDelegate.Broadcast(); + } +} + +void UControllerMenuWidget::OnDownPressedEvent() +{ + if (!this->IsAnyAnimationPlaying() && this->bIsInteractable) + { + //const FControllerWidgetFocusMap &WidgetFocusMap = this->WidgetFocusMapArray[this->FocusedWidgetIndex]; + //if (WidgetFocusMap.Down) + //{ + // this->FocusedWidgetIndex = this->FocusableWidgetArray.Find(WidgetFocusMap.Down); + // this->ResetFocus(); + //} + + this->NativeOnDownPressed(); + this->OnDownPressed(); + this->OnDownPressedDelegate.Broadcast(); + } +} + +void UControllerMenuWidget::OnLeftPressedEvent() +{ + if (!this->IsAnyAnimationPlaying() && this->bIsInteractable && this->FocusedWidgetIndex >= 0) + { + const FControllerWidgetFocusMap& WidgetFocusMap = this->WidgetFocusMapArray[this->FocusedWidgetIndex]; + if (!WidgetFocusMap.Left) + { + if (UTextSpinner* TextSpinner = Cast(this->GetFocusedWidget())) + { + TextSpinner->PreviousItem(); + } + } + + this->NativeOnLeftPressed(); + this->OnLeftPressed(); + this->OnLeftPressedDelegate.Broadcast(); + } +} + +void UControllerMenuWidget::OnRightPressedEvent() +{ + if (!this->IsAnyAnimationPlaying() && this->bIsInteractable && this->FocusedWidgetIndex >= 0) + { + const FControllerWidgetFocusMap& WidgetFocusMap = this->WidgetFocusMapArray[this->FocusedWidgetIndex]; + if (!WidgetFocusMap.Right) + { + if (UTextSpinner* TextSpinner = Cast(this->GetFocusedWidget())) + { + TextSpinner->NextItem(); + } + } + + this->NativeOnRightPressed(); + this->OnRightPressed(); + this->OnRightPressedDelegate.Broadcast(); + } +} + +void UControllerMenuWidget::OnPreviousPressedEvent() +{ + if (!this->IsAnyAnimationPlaying() && this->bIsInteractable) + { + //const FControllerWidgetFocusMap &WidgetFocusMap = this->WidgetFocusMapArray[this->FocusedWidgetIndex]; + //if (WidgetFocusMap.Previous) + //{ + // this->FocusedWidgetIndex = this->FocusableWidgetArray.Find(WidgetFocusMap.Previous); + // this->ResetFocus(); + //} + + this->NativeOnPreviousPressed(); + this->OnPreviousPressed(); + this->OnPreviousPressedDelegate.Broadcast(); + } +} + +void UControllerMenuWidget::OnNextPressedEvent() +{ + if (!this->IsAnyAnimationPlaying() && this->bIsInteractable) + { + //const FControllerWidgetFocusMap &WidgetFocusMap = this->WidgetFocusMapArray[this->FocusedWidgetIndex]; + //if (WidgetFocusMap.Next) + //{ + // this->FocusedWidgetIndex = this->FocusableWidgetArray.Find(WidgetFocusMap.Next); + // this->ResetFocus(); + //} + + this->NativeOnNextPressed(); + this->OnNextPressed(); + this->OnNextPressedDelegate.Broadcast(); + } +} + +void UControllerMenuWidget::OnAcceptPressedEvent() +{ + if (!this->IsAnyAnimationPlaying() && this->bIsInteractable) + { + this->NativeOnAcceptPressed(); + this->OnAcceptPressed(); + this->OnAcceptPressedDelegate.Broadcast(); + } +} + +void UControllerMenuWidget::OnCancelPressedEvent() +{ + if (!this->IsAnyAnimationPlaying() && this->bIsInteractable) + { + this->NativeOnCancelPressed(); + this->OnCancelPressed(); + this->OnCancelPressedDelegate.Broadcast(); + } +} + +void UControllerMenuWidget::OnPausePressedEvent() +{ + if (!this->IsAnyAnimationPlaying() && this->bIsInteractable) + { + this->NativeOnPausePressed(); + this->OnPausePressed(); + this->OnPausePressedDelegate.Broadcast(); + } +} + +void UControllerMenuWidget::OnCustomPressedEvent(const FName CustomAction) +{ + if (this->bIsInteractable) + { + this->NativeOnCustomPressed(CustomAction); + this->OnCustomPressed(CustomAction); + //this->OnCustomPressedDelegate.Broadcast(); + } +} + + +void UControllerMenuWidget::OnAcceptReleasedEvent() +{ + if (!this->IsAnyAnimationPlaying() && this->bIsInteractable) + { + this->NativeOnAcceptReleased(); + } +} + +void UControllerMenuWidget::OnCancelReleasedEvent() +{ + if (!this->IsAnyAnimationPlaying() && this->bIsInteractable) + { + this->NativeOnCancelReleased(); + } +} + + +void UControllerMenuWidget::SetWidgetFocus(UWidget *NewWidget) +{ + if (NewWidget && (NewWidget->IsVisible() && NewWidget->GetIsEnabled())) + { + if (UWidget *FocusedWidget = this->GetFocusedWidget()) + { + if (ITakesControllerFocus *FocusedControllerWidget = Cast(FocusedWidget)) + { + FocusedControllerWidget->Execute_Unhover(FocusedWidget); + } + else if (UButton *FocusedButton = Cast(FocusedWidget)) + { + FocusedButton->WidgetStyle.Normal = this->NormalStyles[FocusedWidget]; + FocusedButton->WidgetStyle.NormalPadding = this->NormalPaddings[FocusedWidget]; + FocusedButton->OnUnhovered.Broadcast(); + } + } + + this->FocusedWidgetIndex = this->FocusableWidgetArray.Find(NewWidget); + if (UWidget *FocusedWidget = this->GetFocusedWidget()) + { + if (ITakesControllerFocus *FocusedControllerWidget = Cast(FocusedWidget)) + { + FocusedControllerWidget->Execute_Hover(FocusedWidget); + } + else if (UButton *FocusedButton = Cast(FocusedWidget)) + { + FocusedButton->WidgetStyle.Normal = this->HoveredStyles[FocusedWidget]; + FocusedButton->WidgetStyle.NormalPadding = this->NormalPaddings[FocusedWidget]; + FocusedButton->OnHovered.Broadcast(); + } + } + + this->FocusChanged(this->GetFocusedWidget()); + } + + // HACK: Enable focus for this widget, focus, then reset focus setting. + // This allows us to keep focus inside the widget while preventing Slate + // from using its own input handling, and also bypasses a warning about + // setting focus to an unfocusable widget. + this->bIsFocusable = true; + this->SetKeyboardFocus(); + this->bIsFocusable = false; + + if (UWidget *FocusedWidget = this->GetFocusedWidget()) + { + this->FindParentScrollBoxRecursive(FocusedWidget, FocusedWidget->GetParent()); + } +} +void UControllerMenuWidget::FindParentScrollBoxRecursive(UWidget *FocusedWidget, UWidget *WidgetParent) +{ + if (WidgetParent != nullptr) + { + if (UScrollBox *ScrollBox = Cast(WidgetParent)) + { + ScrollBox->ScrollWidgetIntoView(FocusedWidget, true); + } + else + { + this->FindParentScrollBoxRecursive(FocusedWidget, WidgetParent->GetParent()); + } + } +} + +bool UControllerMenuWidget::HasWidgetFocus(UWidget* Widget) const +{ + if (Widget) + { + return this->FocusedWidgetIndex == this->FocusableWidgetArray.Find(Widget); + } + return false; +} + +void UControllerMenuWidget::ClearWidgetFocus() +{ + if (UWidget* Widget = this->GetFocusedWidget()) + { + if (ITakesControllerFocus* FocusedControllerWidget = Cast(Widget)) + { + FocusedControllerWidget->Execute_Unhover(Widget); + } + else if (UButton* FocusedButton = Cast(Widget)) + { + FocusedButton->WidgetStyle.Normal = this->NormalStyles[Widget]; + FocusedButton->WidgetStyle.NormalPadding = this->NormalPaddings[Widget]; + } + } + this->FocusedWidgetIndex = -1; + + this->FocusChanged(nullptr); + + // HACK: Enable focus for this widget, focus, then reset focus setting. + // This allows us to keep focus inside the widget while preventing Slate + // from using its own input handling, and also bypasses a warning about + // setting focus to an unfocusable widget. + this->bIsFocusable = true; + this->SetKeyboardFocus(); + this->bIsFocusable = false; +} + +void UControllerMenuWidget::Refocus(const UWidget* FocusedWidget) +{ + // If the current widget is not in focus any more, find a new widget to focus on + if (!FocusedWidget->GetIsEnabled()) + { + const FControllerWidgetFocusMap& FocusMap = this->WidgetFocusMapArray[this->FocusedWidgetIndex]; + if (FocusMap.Down) { this->SetWidgetFocus(FocusMap.Down); } + else if (FocusMap.Right) { this->SetWidgetFocus(FocusMap.Right); } + else if (FocusMap.Up) { this->SetWidgetFocus(FocusMap.Up); } + else if (FocusMap.Left) { this->SetWidgetFocus(FocusMap.Left); } + else if (FocusMap.Next) { this->SetWidgetFocus(FocusMap.Next); } + else if (FocusMap.Previous) { this->SetWidgetFocus(FocusMap.Previous); } + else { this->SetWidgetFocus(*this->FocusableWidgetArray.begin()); } + } +} + + +void UControllerMenuWidget::SetNewInputDevice(const EControllerMenuInputDevice& NewInputDevice) +{ + if (this->LastInputDevice != NewInputDevice) + { + this->LastInputDevice = NewInputDevice; + if (NewInputDevice != EControllerMenuInputDevice::Mouse) + { + this->MouseMovementStartPosition = FVector2D::ZeroVector; + } + this->InputDeviceChanged(NewInputDevice); + } +} + +FName UControllerMenuWidget::GetInputDeviceName() const +{ + switch (this->LastInputDevice) + { + case EControllerMenuInputDevice::Mouse: + return TEXT("Mouse"); + case EControllerMenuInputDevice::Touchscreen: + return TEXT("Touchscreen"); + case EControllerMenuInputDevice::Keyboard: + return TEXT("Keyboard"); + case EControllerMenuInputDevice::Controller: + return TEXT("Controller"); + } + return TEXT("None"); +} diff --git a/Source/ControllerMenu/Public/ControllerMenuGlobalSettings.h b/Source/ControllerMenu/Public/ControllerMenuGlobalSettings.h new file mode 100644 index 0000000..28c9ae6 --- /dev/null +++ b/Source/ControllerMenu/Public/ControllerMenuGlobalSettings.h @@ -0,0 +1,147 @@ +/** +* Copyright ©2019 BattyBovine +* +* Permission is hereby granted, free of charge, to any person obtaining a +* copy of this software and associated documentation files (the "Software"), +* to deal in the Software without restriction, including without limitation +* the rights to use, copy, modify, merge, publish, distribute, sublicense, +* and/or sell copies of the Software, and to permit persons to whom the +* Software is furnished to do so, subject to the following conditions: + +* The above copyright notice and this permission notice shall be included in +* all copies or substantial portions of the Software. + +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +* DEALINGS IN THE SOFTWARE. +**/ + + +#pragma once + +#include "Engine/DeveloperSettings.h" + +#include "ControllerMenuGlobalSettings.generated.h" + + +/** + * Global settings for ControllerMenu classes + */ +UCLASS(Config=Game, defaultconfig, meta = (DisplayName = "ControllerMenu")) +class CONTROLLERMENU_API UControllerMenuGlobalSettings : public UDeveloperSettings +{ + GENERATED_BODY() + +public: + UPROPERTY(config, EditAnywhere, Category = "Input | Axis", meta = ( + DisplayName = "AxisY Action", + ToolTip = "Input action for pressing up and down in a menu using a joystick axis.\nIf this is set, it will override Up/Down action mappings.", + ConfigRestartRequired = false)) + FString AxisYAction; + + UPROPERTY(config, EditAnywhere, Category = "Input | Axis", meta = ( + DisplayName = "AxisX Action", + ToolTip = "Input action for pressing left and right in a menu using a joystick axis.\nIf this is set, it will override Left/Right action mappings.", + ConfigRestartRequired = false)) + FString AxisXAction; + + UPROPERTY(config, EditAnywhere, Category = "Input | Axis", meta = ( + DisplayName = "Axis Movement Threshold", + ToolTip = "Sets how far the joystick must move before menu movement is recognised.", + ConfigRestartRequired = false)) + float AxisMovementThreshold = 0.75; + + + UPROPERTY(config, EditAnywhere, Category = "Input | Actions", meta = ( + DisplayName = "Up Action", + ToolTip = "Input action for pressing up in a menu.\nWill not be used if an AxisY action is set.", + ConfigRestartRequired = false)) + FString UpAction; + + UPROPERTY(config, EditAnywhere, Category = "Input | Actions", meta = ( + DisplayName = "Down Action", + ToolTip = "Input action for pressing down in a menu.\nWill not be used if an AxisY action is set.", + ConfigRestartRequired = false)) + FString DownAction; + + UPROPERTY(config, EditAnywhere, Category = "Input | Actions", meta = ( + DisplayName = "Left Action", + ToolTip = "Input action for pressing left in a menu.\nWill not be used if an AxisX action is set.", + ConfigRestartRequired = false)) + FString LeftAction; + + UPROPERTY(config, EditAnywhere, Category = "Input | Actions", meta = ( + DisplayName = "Right Action", + ToolTip = "Input action for pressing right in a menu.\nWill not be used if an AxisX action is set.", + ConfigRestartRequired = false)) + FString RightAction; + + UPROPERTY(config, EditAnywhere, Category = "Input | Actions", meta = ( + DisplayName = "Previous Action", + ToolTip = "Input action for navigating to the previous item in a menu.", + ConfigRestartRequired = false)) + FString PreviousAction; + + UPROPERTY(config, EditAnywhere, Category = "Input | Actions", meta = ( + DisplayName = "Next Action", + ToolTip = "Input action for navigating to the next item in a menu.", + ConfigRestartRequired = false)) + FString NextAction; + + UPROPERTY(config, EditAnywhere, Category = "Input | Actions", meta = ( + DisplayName = "Accept Action", + ToolTip = "Input action for pressing the accept button in a menu.", + ConfigRestartRequired = false)) + FString AcceptAction; + + UPROPERTY(config, EditAnywhere, Category = "Input | Actions", meta = ( + DisplayName = "Cancel Action", + ToolTip = "Input action for pressing the cancel button in a menu.", + ConfigRestartRequired = false)) + FString CancelAction; + + UPROPERTY(config, EditAnywhere, Category = "Input | Actions", meta = ( + DisplayName = "Pause Action", + ToolTip = "Input action for pressing the pause button in a menu.", + ConfigRestartRequired = false)) + FString PauseAction; + + + UPROPERTY(config, EditAnywhere, Category = "Input | Mouse", meta = ( + DisplayName = "Mouse Movement Threshold", + ToolTip = "Sets the distance the mouse needs to move before the menu passes control to it.", + ConfigRestartRequired = false)) + float MouseMovementThreshold = 2.0; + + + UPROPERTY(config, EditAnywhere, Category = "Widgets | ControllerMenu", meta = ( + DisplayName = "Cancel Confirm", + ToolTip = "If enabled, the \"Cancel\" action will set focus to the menu's cancel button if it's not in focus, then a second press will activate the button. If disabled, the cancel action will simply press the cancel button directly.", + ConfigRestartRequired = false)) + bool CancelConfirm = true; + + + UPROPERTY(config, EditAnywhere, Category = "Widgets | Text Spinner", meta = ( + DisplayName = "Previous Text", + ToolTip = "Change the string used for the \"Previous\" button in the text spinner widget.", + ConfigRestartRequired = false)) + FString TextSpinnerPreviousText = "<"; + + UPROPERTY(config, EditAnywhere, Category = "Widgets | Text Spinner", meta = ( + DisplayName = "Next Text", + ToolTip = "Change the string used for the \"Next\" button in the text spinner widget.", + ConfigRestartRequired = false)) + FString TextSpinnerNextText = ">"; + +public: + virtual void PostInitProperties() override; + virtual FName GetCategoryName() const override; + +#if WITH_EDITOR + virtual void PostEditChangeProperty(struct FPropertyChangedEvent& PropertyChangedEvent) override; +#endif +}; diff --git a/Source/ControllerMenu/Public/IControllerMenu_Implementation.h b/Source/ControllerMenu/Public/IControllerMenu_Implementation.h new file mode 100644 index 0000000..4b4a6e5 --- /dev/null +++ b/Source/ControllerMenu/Public/IControllerMenu_Implementation.h @@ -0,0 +1,45 @@ +/** + * Copyright ©2019 Batty Bovine Productions, LLC. All Rights Reserved. + **/ + + +#pragma once + +#include "CoreMinimal.h" +#include "Modules/ModuleInterface.h" +#include "Modules/ModuleManager.h" + + +DECLARE_LOG_CATEGORY_EXTERN(ControllerMenuLog, Log, All); + +/** + * The public interface to this module. In most cases, this interface is only public to sibling modules + * within this plugin. + */ +class IControllerMenu_Implementation : public IModuleInterface +{ + +public: + + /** + * Singleton-like access to this module's interface. This is just for convenience! + * Beware of calling this during the shutdown phase, though. Your module might have been unloaded already. + * + * @return Returns singleton instance, loading the module on demand if needed + */ + static inline IControllerMenu_Implementation& Get() + { + return FModuleManager::LoadModuleChecked("ControllerMenu"); + } + + /** + * Checks to see if this module is loaded and ready. It is only valid to call Get() if IsAvailable() returns true. + * + * @return True if the module is loaded and ready to use + */ + static inline bool IsAvailable() + { + return FModuleManager::Get().IsModuleLoaded("ControllerMenu"); + } +}; + diff --git a/Source/ControllerMenu/Public/Interfaces/ITakesControllerFocus.h b/Source/ControllerMenu/Public/Interfaces/ITakesControllerFocus.h new file mode 100644 index 0000000..65843f1 --- /dev/null +++ b/Source/ControllerMenu/Public/Interfaces/ITakesControllerFocus.h @@ -0,0 +1,49 @@ +/** + * Copyright ©2019 Batty Bovine Productions, LLC. All Rights Reserved. + **/ + + +#pragma once + +#include "CoreMinimal.h" +#include "ITakesControllerFocus.generated.h" + + +UINTERFACE(BlueprintType) +class CONTROLLERMENU_API UTakesControllerFocus : public UInterface +{ + GENERATED_BODY() +}; +class CONTROLLERMENU_API ITakesControllerFocus +{ + GENERATED_BODY() + +public: + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="ControllerMenu") + void Press(); + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="ControllerMenu") + void Release(); + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="ControllerMenu") + void Hover(); + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="ControllerMenu") + void Unhover(); + + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="ControllerMenu") + FSlateBrush GetNormalStyle(); + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="ControllerMenu") + FSlateBrush GetHoveredStyle(); + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="ControllerMenu") + FSlateBrush GetPressedStyle(); + + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="ControllerMenu") + FMargin GetNormalPadding(); + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="ControllerMenu") + FMargin GetPressedPadding(); + + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="ControllerMenu") + void SetFocusable(bool Focusable); + +protected: + FSlateBrush NormalBrush; + FMargin NormalPadding; +}; diff --git a/Source/ControllerMenu/Public/Widgets/Components/ControllerButton.h b/Source/ControllerMenu/Public/Widgets/Components/ControllerButton.h new file mode 100644 index 0000000..3bd0a0a --- /dev/null +++ b/Source/ControllerMenu/Public/Widgets/Components/ControllerButton.h @@ -0,0 +1,137 @@ +/** + * Copyright ©2019 Batty Bovine Productions, LLC. All Rights Reserved. + **/ + + +#pragma once + +#include "CoreMinimal.h" + +#include "ITakesControllerFocus.h" +#include "Components/Button.h" +#include "Components/ContentWidget.h" + +#include "ControllerButton.generated.h" + + +UENUM(BlueprintType) +enum class EControllerButtonState : uint8 +{ + Normal, + Hovered, + Pressed +}; + + +/** + * A custom button that supports progammatic hover and press behaviour + */ +UCLASS(Blueprintable, BlueprintType, meta=(DisplayName="Button")) +class CONTROLLERMENU_API UControllerButton : public UContentWidget, public ITakesControllerFocus +{ + GENERATED_BODY() + +public: + UControllerButton(const FObjectInitializer& ObjectInitializer); + + /** Sets the current enabled status of the widget */ + virtual void SetIsEnabled(bool bInIsEnabled) override { Super::SetIsEnabled(bInIsEnabled); this->OnFocusChanged.Broadcast(); } + + + /** The button style used at runtime */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Appearance", meta=(DisplayName="Button Style")) + FButtonStyle ButtonStyle; + + + /** BEGIN ITakesControllerFocus implementation */ + virtual void Press_Implementation() override; + virtual void Release_Implementation() override; + virtual void Hover_Implementation() override; + virtual void Unhover_Implementation() override; + + virtual FSlateBrush GetNormalStyle_Implementation() override { return this->NormalBrush; } + virtual FSlateBrush GetHoveredStyle_Implementation() override { return this->ButtonStyle.Hovered; } + virtual FSlateBrush GetPressedStyle_Implementation() override { return this->ButtonStyle.Pressed; } + + virtual FMargin GetNormalPadding_Implementation() override { return this->NormalPadding; } + virtual FMargin GetPressedPadding_Implementation() override { return this->ButtonStyle.PressedPadding; } + + virtual void SetFocusable_Implementation(bool Focusable) override { this->bIsFocusable = Focusable; } + /** END ITakesControllerFocus implementation */ + + + /** Called when the button is clicked */ + UPROPERTY(BlueprintAssignable, Category="Button | Event") + FOnButtonClickedEvent OnClicked; + + /** Called when the button is pressed */ + UPROPERTY(BlueprintAssignable, Category="Button | Event") + FOnButtonPressedEvent OnPressed; + + /** Called when the main button is released */ + UPROPERTY(BlueprintAssignable, Category="Button | Event") + FOnButtonReleasedEvent OnReleased; + + /** Called when the button is hovered */ + UPROPERTY(BlueprintAssignable, Category="Button | Event") + FOnButtonHoverEvent OnHovered; + + /** Called when the button is unhovered */ + UPROPERTY(BlueprintAssignable, Category="Button | Event") + FOnButtonHoverEvent OnUnhovered; + + /** The type of mouse action required by the user to trigger the buttons' 'Click' */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Interaction", AdvancedDisplay) + TEnumAsByte ClickMethod; + + /** The type of touch action required by the user to trigger the buttons' 'Click' */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Interaction", AdvancedDisplay) + TEnumAsByte TouchMethod; + + /** The type of keyboard/gamepad button press action required by the user to trigger the buttons' 'Click' */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Interaction", AdvancedDisplay) + TEnumAsByte PressMethod; + + DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnFocusChangedEvent); + UPROPERTY(BlueprintAssignable, Category = "ControllerMenu | Event") + FOnFocusChangedEvent OnFocusChanged; + + + /** Whether the button can be focused with the keyboard or gamepad */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Interaction") + uint8 bIsFocusable : 1; + + + TSharedPtr SlateButton; + +#if WITH_EDITOR + virtual const FText GetPaletteCategory() override { return NSLOCTEXT("Button", "ControllerMenu", "ControllerMenu"); } +#endif + +protected: + /** BEGIN UWidget Interface */ + virtual TSharedRef RebuildWidget() override; + //virtual void SynchronizeProperties() override; + /** END UWidget Interface */ + + /** BEGIN UVisual Interface */ + virtual void ReleaseSlateResources(bool bReleaseChildren) override; + /** END UVisual Interface */ + + /** BEGIN UPanelWidget Interface */ + virtual UClass* GetSlotClass() const override; + virtual void OnSlotAdded(UPanelSlot* Slot) override; + virtual void OnSlotRemoved(UPanelSlot* Slot) override; + /** END UPanelWidget Interface */ + + FReply SlateHandleClicked(); + void SlateHandlePressed(); + void SlateHandleReleased(); + void SlateHandleHovered(); + void SlateHandleUnhovered(); + +private: + struct FButtonStyle* DefaultControllerButtonStyle = nullptr; + + EControllerButtonState ControllerButtonState = EControllerButtonState::Normal; +}; diff --git a/Source/ControllerMenu/Public/Widgets/Components/TextSpinner.h b/Source/ControllerMenu/Public/Widgets/Components/TextSpinner.h new file mode 100644 index 0000000..f99b180 --- /dev/null +++ b/Source/ControllerMenu/Public/Widgets/Components/TextSpinner.h @@ -0,0 +1,335 @@ +/** + * Copyright ©2019 Batty Bovine Productions, LLC. All Rights Reserved. + **/ + +#pragma once + +#include "CoreMinimal.h" + +#include "ITakesControllerFocus.h" +#include "Components/Button.h" + +#include "TextSpinner.generated.h" + + +UENUM(BlueprintType) +enum class ETextSpinnerState : uint8 +{ + Normal, + Hovered, + Pressed +}; + + +/** + * A spinner box that displays text instead of numbers + */ +UCLASS(Blueprintable, BlueprintType, meta=(DisplayName="Text Spinner")) +class CONTROLLERMENU_API UTextSpinner : public UWidget, public ITakesControllerFocus +{ + GENERATED_BODY() + +public: + UTextSpinner(const FObjectInitializer& ObjectInitializer); + + + /** Set the selected item to the specified item, if possible */ + UFUNCTION(BlueprintCallable) + void SetSelectedItem(const FText Item/*, ESelectInfo::Type SelectionType = ESelectInfo::Direct*/); + /** Get the selected item */ + UFUNCTION(BlueprintPure) + FText GetSelectedItem() const { if (this->SelectedItemIndex >= 0 && this->Items.Num()) { return this->Items[this->SelectedItemIndex]; } return FText(); } + + /** Select the item at the specified index */ + UFUNCTION(BlueprintCallable) + bool SetSelectedIndex(const int32 Index/*, ESelectInfo::Type SelectionType = ESelectInfo::Direct*/); + /** Get the index of the selected item */ + UFUNCTION(BlueprintPure) + int32 GetSelectedIndex() const { return this->SelectedItemIndex; } + + /** Add an item to the list */ + UFUNCTION(BlueprintCallable) + void AddItem(const FText Item); + + /** Switch to the previous item in the list */ + UFUNCTION(BlueprintCallable) + void PreviousItem(); + + /** Switch to the next item in the list */ + UFUNCTION(BlueprintCallable) + void NextItem(); + + UFUNCTION(BlueprintPure) + int32 FindItemIndex(const FText& Item) const; + + UFUNCTION(BlueprintPure) + FText GetItemAtIndex(int32 Index) { return (Index >= 0 && Index < this->Items.Num()) ? this->Items[Index] : FText(); } + + UFUNCTION(BlueprintCallable) + void ClearItems() { this->Items.Empty(); } + + + /** BEGIN ITakesControllerFocus interface */ + virtual void Press_Implementation() override; + virtual void Release_Implementation() override; + virtual void Hover_Implementation() override; + virtual void Unhover_Implementation() override; + + virtual FSlateBrush GetNormalStyle_Implementation() override { return this->NormalBrush; } + virtual FSlateBrush GetHoveredStyle_Implementation() override { return this->OptionButtonStyle.Hovered; } + virtual FSlateBrush GetPressedStyle_Implementation() override { return this->OptionButtonStyle.Pressed; } + + virtual FMargin GetNormalPadding_Implementation() override { return this->NormalPadding; } + virtual FMargin GetPressedPadding_Implementation() override { return this->OptionButtonStyle.PressedPadding; } + + //virtual void SetFocusable_Implementation(bool Focusable) override { this->bIsFocusable = Focusable; } + /** END ITakesControllerFocus interface */ + + + /** The list of items to cycle through */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Text Spinner") + TArray Items; + + /** If true, items will loop around when scrolling past the last item in the list */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Text Spinner") + uint8 bLoop : 1; + + + /** Bindings for Option Text colour */ + PROPERTY_BINDING_IMPLEMENTATION(FSlateColor, OptionTextColorAndOpacity); + PROPERTY_BINDING_IMPLEMENTATION(FSlateColor, OptionTextColorAndOpacityHovered); + PROPERTY_BINDING_IMPLEMENTATION(FSlateColor, OptionTextColorAndOpacityPressed); + PROPERTY_BINDING_IMPLEMENTATION(FLinearColor, OptionTextShadowColorAndOpacity); + + /** Bindings for Previous/Next Text colour */ + PROPERTY_BINDING_IMPLEMENTATION(FSlateColor, ButtonTextColorAndOpacity); + + + /** The button style used at runtime */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Appearance", meta=(DisplayName="Button Style")) + FButtonStyle OptionButtonStyle; + + /** The button style used at runtime */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Appearance", meta=(DisplayName="Previous/Next Style")) + FButtonStyle PreviousNextButtonStyle; + + /** Text justification */ + UPROPERTY(Category = "Text | Spinner", EditAnywhere, BlueprintReadOnly) + TEnumAsByte OptionTextJustification; + + /** Margin */ + UPROPERTY(Category = "Text | Spinner", EditAnywhere, BlueprintReadOnly) + FMargin OptionTextMargin; + + /** Normal Color and Opacity */ + UPROPERTY(Category = "Text | Spinner", EditAnywhere, BlueprintReadOnly) + FSlateColor OptionTextColorAndOpacity; + + /** A delegate for Normal Color and Opacity */ + UPROPERTY() + FGetSlateColor OptionTextColorAndOpacityDelegate; + + /** Hover Color and Opacity */ + UPROPERTY(Category = "Text | Spinner", EditAnywhere, BlueprintReadOnly) + FSlateColor OptionTextColorAndOpacityHovered; + + /** A delegate for Hover Color and Opacity */ + UPROPERTY() + FGetSlateColor OptionTextColorAndOpacityHoveredDelegate; + + /** Pressed Color and Opacity */ + UPROPERTY(Category = "Text | Spinner", EditAnywhere, BlueprintReadOnly) + FSlateColor OptionTextColorAndOpacityPressed; + + /** A delegate for Pressed Color and Opacity */ + UPROPERTY() + FGetSlateColor OptionTextColorAndOpacityPressedDelegate; + + /** The main text font */ + UPROPERTY(Category = "Text | Spinner", EditAnywhere, BlueprintReadOnly) + FSlateFontInfo OptionTextFont; + + /** The direction the shadow is cast */ + UPROPERTY(Category = "Text | Spinner", EditAnywhere, BlueprintReadOnly) + FVector2D OptionTextShadowOffset; + + /** Shadow Color */ + UPROPERTY(Category = "Text | Spinner", EditAnywhere, BlueprintReadOnly, meta = (DisplayName = "Shadow Color")) + FLinearColor OptionTextShadowColorAndOpacity; + + /** A delegate for the Shadow Color and Opacity */ + UPROPERTY() + FGetLinearColor OptionTextShadowColorAndOpacityDelegate; + + /** The button font */ + UPROPERTY(Category = "Text | Buttons", EditAnywhere, BlueprintReadOnly, meta = (DisplayName = "Text Font")) + FSlateFontInfo ButtonTextFont; + + /** Button colour and opacity */ + UPROPERTY(Category = "Text | Buttons", EditAnywhere, BlueprintReadOnly, meta = (DisplayName = "Text Color And Opacity")) + FSlateColor ButtonTextColorAndOpacity; + + /** A delegate for button colour and opacity */ + UPROPERTY() + FGetSlateColor ButtonTextColorAndOpacityDelegate; + + /** Button shadow colour */ + UPROPERTY(Category = "Text | Buttons", EditAnywhere, BlueprintReadOnly, meta = (DisplayName = "Shadow Color")) + FLinearColor ButtonTextShadowColorAndOpacity; + + /** A delegate for the button shadow colour and opacity */ + UPROPERTY() + FGetLinearColor ButtonTextShadowColorAndOpacityDelegate; + + /** Button margin */ + UPROPERTY(Category = "Text | Buttons", EditAnywhere, BlueprintReadOnly, meta = (DisplayName = "Text Margin")) + FMargin ButtonTextMargin; + + DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnSelectionChangedEvent, FText, SelectedItem, int32, SelectedIndex/*, ESelectInfo::Type, SelectionType*/); + UPROPERTY(BlueprintAssignable, Category=Events) + FOnSelectionChangedEvent OnSelectionChanged; + + UPROPERTY(Category = "Text Spinner", EditAnywhere, BlueprintReadWrite) + uint8 bEmitSelectionChangedEvents : 1; + + + /** Called when the main button is clicked */ + UPROPERTY(BlueprintAssignable, Category="Button | Event") + FOnButtonClickedEvent OnClicked; + + /** Called when the main button is pressed */ + UPROPERTY(BlueprintAssignable, Category="Button | Event") + FOnButtonPressedEvent OnPressed; + + /** Called when the main button is released */ + UPROPERTY(BlueprintAssignable, Category="Button | Event") + FOnButtonReleasedEvent OnReleased; + + /** Called when the main button is hovered */ + UPROPERTY(BlueprintAssignable, Category="Button | Event") + FOnButtonHoverEvent OnHovered; + + /** Called when the main button is unhovered */ + UPROPERTY(BlueprintAssignable, Category="Button | Event") + FOnButtonHoverEvent OnUnhovered; + + + /** Called when the "Previous" button is clicked */ + UPROPERTY(BlueprintAssignable, Category="Previous Button | Event") + FOnButtonClickedEvent OnPreviousClicked; + + /** Called when the "Previous" button is pressed */ + UPROPERTY(BlueprintAssignable, Category="Previous Button | Event") + FOnButtonPressedEvent OnPreviousPressed; + + /** Called when the "Previous" button is released */ + UPROPERTY(BlueprintAssignable, Category="Previous Button | Event") + FOnButtonReleasedEvent OnPreviousReleased; + + /** Called when the "Previous" button is hovered */ + UPROPERTY(BlueprintAssignable, Category="Previous Button | Event") + FOnButtonHoverEvent OnPreviousHovered; + + /** Called when the "Previous" button is unhovered */ + UPROPERTY(BlueprintAssignable, Category="Previous Button | Event") + FOnButtonHoverEvent OnPreviousUnhovered; + + + /** Called when the "Next" button is clicked */ + UPROPERTY(BlueprintAssignable, Category="Next Button | Event") + FOnButtonClickedEvent OnNextClicked; + + /** Called when the "Next" button is pressed */ + UPROPERTY(BlueprintAssignable, Category="Next Button | Event") + FOnButtonPressedEvent OnNextPressed; + + /** Called when the "Next" button is released */ + UPROPERTY(BlueprintAssignable, Category="Next Button | Event") + FOnButtonReleasedEvent OnNextReleased; + + /** Called when the "Next" button is hovered */ + UPROPERTY(BlueprintAssignable, Category="Next Button | Event") + FOnButtonHoverEvent OnNextHovered; + + /** Called when the "Next" button is unhovered */ + UPROPERTY(BlueprintAssignable, Category="Next Button | Event") + FOnButtonHoverEvent OnNextUnhovered; + + /** The type of mouse action required by the user to trigger the buttons' 'Click' */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Interaction", AdvancedDisplay) + TEnumAsByte ClickMethod; + + /** The type of touch action required by the user to trigger the buttons' 'Click' */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Interaction", AdvancedDisplay) + TEnumAsByte TouchMethod; + + /** The type of keyboard/gamepad button press action required by the user to trigger the buttons' 'Click' */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Interaction", AdvancedDisplay) + TEnumAsByte PressMethod; + + + /** Sometimes a button should only be mouse-clickable and never keyboard focusable. */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Interaction") + uint8 bIsFocusable : 1; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Interaction") + uint8 bIsPressable : 1; + + + /** Horizontal alignment box for our compound widget */ + TSharedPtr ParentHBox; + + /** Widgets to comprise the "Previous" button */ + TSharedPtr PreviousButton; + TSharedPtr PreviousButtonText; + + /** Button to let the spinner act as a button in itself */ + TSharedPtr OptionButton; + TSharedPtr OptionText; + + /** Widgets to comprise the "Next" button */ + TSharedPtr NextButton; + TSharedPtr NextButtonText; + +#if WITH_EDITOR + virtual const FText GetPaletteCategory() override { return NSLOCTEXT("TextSpinner", "ControllerMenu", "ControllerMenu"); } +#endif + +protected: + /** BEGIN UWidget Interface */ + virtual TSharedRef RebuildWidget() override; + virtual void SynchronizeProperties() override; + /** END UWidget Interface */ + + /** BEGIN UVisual Interface */ + virtual void ReleaseSlateResources(bool bReleaseChildren) override; + /** END UVisual Interface */ + + FReply SlateHandleClicked(); + void SlateHandlePressed(); + void SlateHandleReleased(); + void SlateHandleHovered(); + void SlateHandleUnhovered(); + + FReply SlateHandleClickedPrevious(); + void SlateHandlePressedPrevious(); + void SlateHandleReleasedPrevious(); + void SlateHandleHoveredPrevious(); + void SlateHandleUnhoveredPrevious(); + + FReply SlateHandleClickedNext(); + void SlateHandlePressedNext(); + void SlateHandleReleasedNext(); + void SlateHandleHoveredNext(); + void SlateHandleUnhoveredNext(); + +private: + struct FButtonStyle* DefaultSpinnerButtonStyle = nullptr; + + int32 SelectedItemIndex = 0; + + FString PreviousTextString; + FString NextTextString; + + ETextSpinnerState OptionButtonState = ETextSpinnerState::Normal; +}; diff --git a/Source/ControllerMenu/Public/Widgets/ControllerMenuMessageBoxWidget.h b/Source/ControllerMenu/Public/Widgets/ControllerMenuMessageBoxWidget.h new file mode 100644 index 0000000..1ba428f --- /dev/null +++ b/Source/ControllerMenu/Public/Widgets/ControllerMenuMessageBoxWidget.h @@ -0,0 +1,43 @@ +/** + * Copyright ©2019 Batty Bovine Productions, LLC. All Rights Reserved. + **/ + + +#pragma once + +#include "CoreMinimal.h" + +#include "Widgets/ControllerMenuWidget.h" + +#include "ControllerMenuMessageBoxWidget.generated.h" + +/** + * Display a message box with accept and cancel buttons + */ +UCLASS() +class CONTROLLERMENU_API UControllerMenuMessageBoxWidget : public UControllerMenuWidget +{ + GENERATED_BODY() + +public: + void SetMessageBody(const FText& Message); + void SetAcceptText(const FText& Accept); + void SetCancelText(const FText& Cancel); + + UFUNCTION() void Accepted(); + UFUNCTION() void Cancelled(); + + DECLARE_DYNAMIC_MULTICAST_DELEGATE(FMessageBoxChoiceEvent); + UPROPERTY(BlueprintAssignable) FMessageBoxChoiceEvent OnAccepted; + UPROPERTY(BlueprintAssignable) FMessageBoxChoiceEvent OnCancelled; + +protected: + virtual bool Initialize() override; + + UPROPERTY(BlueprintReadWrite, meta=(BindWidget)) class UControllerButton* AcceptButton = nullptr; + UPROPERTY(BlueprintReadWrite, meta=(BindWidget)) class UControllerButton* CancelButton = nullptr; + + UPROPERTY(BlueprintReadWrite, meta=(BindWidget)) class UTextBlock* MessageBody = nullptr; + UPROPERTY(BlueprintReadWrite, meta=(BindWidget)) class UTextBlock* AcceptText = nullptr; + UPROPERTY(BlueprintReadWrite, meta=(BindWidget)) class UTextBlock* CancelText = nullptr; +}; diff --git a/Source/ControllerMenu/Public/Widgets/ControllerMenuWidget.h b/Source/ControllerMenu/Public/Widgets/ControllerMenuWidget.h new file mode 100644 index 0000000..3a9b490 --- /dev/null +++ b/Source/ControllerMenu/Public/Widgets/ControllerMenuWidget.h @@ -0,0 +1,346 @@ +/** + * Copyright ©2019 Batty Bovine Productions, LLC. All Rights Reserved. + **/ + +#pragma once + +#include "CoreMinimal.h" +#include "Blueprint/UserWidget.h" + +#include "ControllerMenuWidget.generated.h" + + +USTRUCT() +struct FControllerWidgetFocusMap +{ + GENERATED_BODY() + UWidget* Up = nullptr; + UWidget* Down = nullptr; + UWidget* Left = nullptr; + UWidget* Right = nullptr; + UWidget* Previous = nullptr; + UWidget* Next = nullptr; +}; + +UENUM(BlueprintType) +enum class EControllerMenuInputDevice : uint8 +{ + None UMETA(DisplayName="None"), + Mouse UMETA(DisplayName="Mouse"), + Touchscreen UMETA(DisplayName="Touchscreen"), + Keyboard UMETA(DisplayName="Keyboard"), + Controller UMETA(DisplayName="Controller") +}; + + +/** + * Static helper functions for working with widgets + */ +UCLASS() +class CONTROLLERMENU_API UControllerMenuWidgetHelperFunctions : public UObject +{ + GENERATED_BODY() + +public: + UFUNCTION(BlueprintPure, Category = "User Interface|Geometry") + static FVector2D GetAbsolutePosition(const FGeometry& Geometry) { return Geometry.GetAbsolutePosition(); } +}; + + +/** + * Parent class for all in-game menus + */ +UCLASS() +class CONTROLLERMENU_API UControllerMenuWidget : public UUserWidget +{ + GENERATED_UCLASS_BODY() + +public: + /** Open and display the menu at the specified depth */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent) + void OpenMenu(int32 Depth = 0); + /** Close the menu and emit a signal notifying other connected menus */ + UFUNCTION(BlueprintCallable, BlueprintNativeEvent) + void ExitMenu(); + + /** Sets which button should exit the menu. The Cancel + button will automatically jump to this button when + pressed, and will exit the menu if pressed again. */ + UFUNCTION(BlueprintCallable) + void SetCancelButton(class UWidget *Button); + + /** Up button press event */ + UFUNCTION(BlueprintImplementableEvent) + void OnUpPressed(); + /** Down button press event */ + UFUNCTION(BlueprintImplementableEvent) + void OnDownPressed(); + /** Left button press event */ + UFUNCTION(BlueprintImplementableEvent) + void OnLeftPressed(); + /** Right button press event */ + UFUNCTION(BlueprintImplementableEvent) + void OnRightPressed(); + /** Previous button press event */ + UFUNCTION(BlueprintImplementableEvent) + void OnPreviousPressed(); + /** Next button press event */ + UFUNCTION(BlueprintImplementableEvent) + void OnNextPressed(); + /** Accept button press event */ + UFUNCTION(BlueprintImplementableEvent) + void OnAcceptPressed(); + /** Cancel button press event */ + UFUNCTION(BlueprintImplementableEvent) + void OnCancelPressed(); + /** Pause button press event */ + UFUNCTION(BlueprintImplementableEvent) + void OnPausePressed(); + /** Custom button press events */ + UFUNCTION(BlueprintImplementableEvent) + void OnCustomPressed(const FName &CustomAction); + + /** Sends the currently focused widget to a Blueprint event when the focus changes */ + UFUNCTION(BlueprintImplementableEvent) + void FocusChanged(UWidget *FocusedWidget); + + /** Set input focus to the given widget */ + UFUNCTION(BlueprintCallable) + void SetWidgetFocus(UWidget *NewWidget); + /** Check if this widget has input focus */ + UFUNCTION(BlueprintPure) + bool HasWidgetFocus(UWidget *Widget) const; + /** Clear input focus */ + UFUNCTION(BlueprintCallable) + void ClearWidgetFocus(); + /** Check current widget focus and refocus if necessary, such as if the focused widget has been disabled */ + UFUNCTION(BlueprintCallable) + void Refocus(const UWidget *FocusedWidget); + /** Get the currently focused widget */ + UFUNCTION(BlueprintPure) + UWidget* GetFocusedWidget() const { if(this->FocusedWidgetIndex>=0&&this->FocusedWidgetIndexFocusableWidgetArray.Num()){return this->FocusableWidgetArray[this->FocusedWidgetIndex];}else{return nullptr;} } + /** Get the index value of the currently focused widget */ + UFUNCTION(BlueprintPure) + int32 GetFocusedWidgetIndex() const { return this->FocusedWidgetIndex; } + + UFUNCTION(BlueprintPure) + TArray GetFocusableWidgets() const { return this->FocusableWidgetArray; } + + /** Get the current input device */ + UFUNCTION(BlueprintPure) + EControllerMenuInputDevice GetInputDevice() const { return this->LastInputDevice; } + /** Get the name of the current input device */ + UFUNCTION(BlueprintPure) + FName GetInputDeviceName() const; + + /** Event called when the input device has changed */ + UFUNCTION(BlueprintImplementableEvent) + void InputDeviceChanged(const EControllerMenuInputDevice &InputDevice); + + /** List of names for custom input actions */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Actions") + TArray CustomActions; + + UPROPERTY(BlueprintReadWrite, Transient, meta=(BindWidgetAnim)) + UWidgetAnimation *OpenAnimation = nullptr; + UPROPERTY(BlueprintReadWrite, Transient, meta=(BindWidgetAnim)) + UWidgetAnimation *CloseAnimation = nullptr; + + DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOpenCloseEvent); + /** Event called when the menu is requested to open */ + UPROPERTY(BlueprintAssignable) + FOpenCloseEvent OnMenuOpened; + /** Event called when the menu is finished transitioning in */ + UPROPERTY(BlueprintAssignable) + FOpenCloseEvent OnMenuShown; + /** Event called when the menu is set to be exited */ + UPROPERTY(BlueprintAssignable) + FOpenCloseEvent OnMenuExited; + /** Event called when the menu is finished shutting down */ + UPROPERTY(BlueprintAssignable) + FOpenCloseEvent OnMenuClosed; + +protected: + void InitializeMenuActions(); + void BindMenuActions(); + void UnbindMenuActions(); + + virtual void OnOpenOverride(){} + virtual void OnCloseOverride(){} + + /** Add focusable widgets arranged in a grid layout panel */ + UFUNCTION(BlueprintCallable, meta=(AdvancedDisplay="1")) + void AddGridPanelWidgets(class UGridPanel *GridPanel); + /** Add a focusable widget beneath the previously added widget */ + UFUNCTION(BlueprintCallable, meta=(AdvancedDisplay="1")) + void AddVerticalFocusableWidget(class UWidget* Widget, class UWidget* Left = nullptr, class UWidget* Right = nullptr); + /** Add a focusable widget to the right of the previously added widget */ + UFUNCTION(BlueprintCallable, meta=(AdvancedDisplay="1")) + void AddHorizontalFocusableWidget(class UWidget* Widget, class UWidget* Up = nullptr, class UWidget* Down = nullptr); + /** Add a focusable widget to the focus stack, and add custom focus widgets for each input action */ + UFUNCTION(BlueprintCallable, meta=(AdvancedDisplay="1")) + void AddFocusableWidgetCustom(class UWidget* Widget, class UWidget* Up = nullptr, class UWidget* Down = nullptr, class UWidget* Left = nullptr, class UWidget* Right = nullptr, class UWidget* Previous = nullptr, class UWidget* Next = nullptr); + UFUNCTION(BlueprintCallable, meta = (AdvancedDisplay = "1")) + void EditWidgetFocus(class UWidget* Widget, class UWidget* Up = nullptr, class UWidget* Down = nullptr, class UWidget* Left = nullptr, class UWidget* Right = nullptr, class UWidget* Previous = nullptr, class UWidget* Next = nullptr); + + /** Event called when the menu been opened */ + UFUNCTION(BlueprintImplementableEvent) + void MenuOpened(); + /** Event called when the menu has finished transitioning in */ + UFUNCTION(BlueprintImplementableEvent) + void MenuShown(); + /** Event called when the menu has been asked to close */ + UFUNCTION(BlueprintImplementableEvent) + void MenuExited(); + /** Event called when the menu has finished closing */ + UFUNCTION(BlueprintImplementableEvent) + void MenuClosed(); + + /** Event called when a child menu has been opened */ + UFUNCTION(BlueprintImplementableEvent) + void ChildMenuOpened(); + /** Event called when the child menu has been asked to close */ + UFUNCTION(BlueprintImplementableEvent) + void ChildMenuExited(); + /** Event called when the child menu has finished closing */ + UFUNCTION(BlueprintImplementableEvent) + void ChildMenuClosed(); + + /** Event called when a message box has been opened */ + UFUNCTION(BlueprintImplementableEvent) + void MessageBoxOpened(); + /** Event called when the message box has been asked to close */ + UFUNCTION(BlueprintImplementableEvent) + void MessageBoxExited(); + /** Event called when the message box has finished closing */ + UFUNCTION(BlueprintImplementableEvent) + void MessageBoxClosed(); + + /** Open a submenu over the current menu, and switch focus */ + UFUNCTION(BlueprintCallable) + void OpenChildMenu(UControllerMenuWidget* Child, bool TransitionOut=true); + + UFUNCTION(BlueprintCallable) + class UControllerMenuMessageBoxWidget* CreateMessageBox(const TSubclassOf Class, const FText Message, const FText AcceptText, const FText CancelText); + + virtual void NativeOnInitialized() override; + virtual FReply NativeOnKeyDown(const FGeometry& InGeometry, const FKeyEvent& InKeyEvent) override; + virtual FReply NativeOnKeyUp(const FGeometry& InGeometry, const FKeyEvent& InKeyEvent) override; + virtual FReply NativeOnMouseMove(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent) override; + virtual FReply NativeOnMouseButtonDown(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent) override; + virtual FReply NativeOnMouseButtonUp(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent) override; + + virtual void NativeOnUpPressed(); + virtual void NativeOnDownPressed(); + virtual void NativeOnLeftPressed(); + virtual void NativeOnRightPressed(); + virtual void NativeOnPreviousPressed(); + virtual void NativeOnNextPressed(); + virtual void NativeOnAcceptPressed(); + virtual void NativeOnCancelPressed(); + virtual void NativeOnPausePressed(){} + virtual void NativeOnCustomPressed(const FName& CustomAction){} + + virtual void NativeOnAcceptReleased(); + virtual void NativeOnCancelReleased(); + + uint8 bIsInteractable : 1; + uint8 bIsChildMenu : 1; + +private: + void OnAxisYEvent(const float AxisValue); + void OnAxisXEvent(const float AxisValue); + + void OnUpPressedEvent(); + void OnDownPressedEvent(); + void OnLeftPressedEvent(); + void OnRightPressedEvent(); + void OnPreviousPressedEvent(); + void OnNextPressedEvent(); + void OnAcceptPressedEvent(); + void OnCancelPressedEvent(); + void OnPausePressedEvent(); + void OnCustomPressedEvent(const FName CustomAction); + + void OnAcceptReleasedEvent(); + void OnCancelReleasedEvent(); + + virtual void ChildMenuExitedEvent(); + virtual void ChildMenuClosedEvent(); + + virtual void MessageBoxExitedEvent(); + virtual void MessageBoxClosedEvent(); + + void SetNewInputDevice(const EControllerMenuInputDevice& NewInputDevice); + + UFUNCTION() void OpenAnimationFinished(); + UFUNCTION() void CloseAnimationFinished(); + + void FindParentScrollBoxRecursive(UWidget *FocusedWidget, UWidget *WidgetParent); + + UControllerMenuWidget* ChildMenu = nullptr; + class UControllerMenuMessageBoxWidget* MessageBox = nullptr; + + uint32 WidgetDepth = 0; + int32 FocusedWidgetIndex = 0; + TArray FocusableWidgetArray; + TArray WidgetFocusMapArray; + class UWidget* CancelButton = nullptr; + bool CancelConfirm = true; + + TMap NormalStyles; + TMap HoveredStyles; + TMap PressedStyles; + TMap NormalPaddings; + TMap PressedPaddings; + + EControllerMenuInputDevice LastInputDevice = EControllerMenuInputDevice::None; + FVector2D MouseMovementStartPosition = FVector2D::ZeroVector; + + /** Sets the movement threshold for joystick axes when moving in the menu */ + float AxisMovementThreshold = 0.75; + /** Flags for when an axis has already moved and additional movements should be ignored */ + uint8 bAxisYHasMoved : 1; + uint8 bAxisXHasMoved : 1; + + /** Sets the distance the mouse needs to move before the menu passes control to it */ + float MouseMovementThreshold = 2.0; + + FInputAxisBinding AxisYBinding; + FInputAxisBinding AxisXBinding; + + FInputActionBinding UpActionBinding; + FInputActionBinding DownActionBinding; + FInputActionBinding LeftActionBinding; + FInputActionBinding RightActionBinding; + FInputActionBinding PreviousActionBinding; + FInputActionBinding NextActionBinding; + FInputActionBinding AcceptPressActionBinding; + FInputActionBinding CancelPressActionBinding; + FInputActionBinding PauseActionBinding; + //FInputActionBinding CustomActionBinding; + + FInputActionBinding AcceptReleaseActionBinding; + FInputActionBinding CancelReleaseActionBinding; + + DECLARE_MULTICAST_DELEGATE(FOnPressedEvent); + FOnPressedEvent OnUpPressedDelegate; + FOnPressedEvent OnDownPressedDelegate; + FOnPressedEvent OnLeftPressedDelegate; + FOnPressedEvent OnRightPressedDelegate; + FOnPressedEvent OnPreviousPressedDelegate; + FOnPressedEvent OnNextPressedDelegate; + FOnPressedEvent OnAcceptPressedDelegate; + FOnPressedEvent OnCancelPressedDelegate; + FOnPressedEvent OnPausePressedDelegate; + + //DECLARE_MULTICAST_DELEGATE_OneParam(FOnPressedEventOneParam, FName); + //FOnPressedEventOneParam OnCustomPressedEvent; + + DECLARE_DELEGATE(FChildMenuClosedEvent); + FChildMenuClosedEvent ChildMenuExitedDelegate; + FChildMenuClosedEvent ChildMenuClosedDelegate; + + TArray StoredPlayerActionBindings; + TArray StoredPlayerAxisBindings; +}; diff --git a/Source/ControllerMenu/Public/Widgets/Slate/SCustomTextBlock.h b/Source/ControllerMenu/Public/Widgets/Slate/SCustomTextBlock.h new file mode 100644 index 0000000..59f0075 --- /dev/null +++ b/Source/ControllerMenu/Public/Widgets/Slate/SCustomTextBlock.h @@ -0,0 +1,29 @@ +/** + * Copyright ©2019 Batty Bovine Productions, LLC. All Rights Reserved. + **/ + + +#pragma once + +#include "CoreMinimal.h" +#include "Widgets/Text/STextBlock.h" + + +/** + * A simple static text widget, altered to suit custom widgets such as UTextSpinner + */ +class SCustomTextBlock : public STextBlock +{ +public: + void SetPreventDisable(bool bInPreventDisable) { this->bPreventDisable = bInPreventDisable; } + + // SWidget interface + virtual int32 OnPaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled) const override + { + return STextBlock::OnPaint(Args, AllottedGeometry, MyCullingRect, OutDrawElements, LayerId, InWidgetStyle, !this->bPreventDisable || bParentEnabled); + } + // End of SWidget interface + +private: + uint8 bPreventDisable : 1; +};