(Feat) Add Main Menu + Music Manger

This commit is contained in:
2026-07-29 20:18:51 +02:00
parent 956f48816b
commit 9b5ad7c8a4
55 changed files with 3727 additions and 125 deletions
@@ -6,7 +6,11 @@
#include "Components/Button.h"
#include "Components/WidgetSwitcher.h"
#include "ConfirmDialogWidget.h"
#include "EnhancedInputSubsystems.h"
#include "Engine/LocalPlayer.h"
#include "InputCoreTypes.h"
#include "KeyCaptureWidget.h"
#include "UserSettings/EnhancedInputUserSettings.h"
#include "SettingsTabWidget.h"
#include "SurvivalUserSettings.h"
@@ -19,7 +23,8 @@ namespace
constexpr int32 TabIndexGameplay = 0;
constexpr int32 TabIndexDisplay = 1;
constexpr int32 TabIndexGraphics = 2;
constexpr int32 TabIndexAudio = 3;
constexpr int32 TabIndexControls = 3;
constexpr int32 TabIndexAudio = 4;
}
USettingsMenuWidget::USettingsMenuWidget(const FObjectInitializer& ObjectInitializer)
@@ -62,16 +67,27 @@ void USettingsMenuWidget::NativeConstruct()
GraphicsTabButton->OnClicked.AddDynamic(this, &USettingsMenuWidget::HandleGraphicsTabClicked);
}
if (ControlsTabButton)
{
ControlsTabButton->OnClicked.AddDynamic(this, &USettingsMenuWidget::HandleControlsTabClicked);
}
if (AudioTabButton)
{
AudioTabButton->OnClicked.AddDynamic(this, &USettingsMenuWidget::HandleAudioTabClicked);
}
if (ResetDialog)
// Chaque onglet recoit un pointeur vers nous : c'est par la qu'il demandera
// une confirmation, sans avoir a fabriquer sa propre boite.
if (TabSwitcher)
{
ResetDialog->OnConfirmed.AddDynamic(this, &USettingsMenuWidget::HandleResetConfirmed);
ResetDialog->OnCancelled.AddDynamic(this, &USettingsMenuWidget::HandleResetCancelled);
ResetDialog->Close();
for (int32 Index = 0; Index < TabSwitcher->GetNumWidgets(); ++Index)
{
if (USettingsTabWidget* Tab = Cast<USettingsTabWidget>(TabSwitcher->GetWidgetAtIndex(Index)))
{
Tab->SetOwningMenu(this);
}
}
}
UpdateTabButtonStates();
@@ -89,6 +105,20 @@ void USettingsMenuWidget::NativeDestruct()
ResetButton->OnClicked.RemoveDynamic(this, &USettingsMenuWidget::HandleResetClicked);
}
// La boite et le voile vivent au viewport, pas dans notre arbre : ils ne
// disparaitraient pas avec nous.
if (ResetDialog)
{
ResetDialog->RemoveFromParent();
ResetDialog = nullptr;
}
if (KeyCapture)
{
KeyCapture->RemoveFromParent();
KeyCapture = nullptr;
}
Super::NativeDestruct();
}
@@ -96,6 +126,9 @@ void USettingsMenuWidget::Open()
{
SetVisibility(ESlateVisibility::Visible);
// L'ecran a pu etre referme sur un autre onglet que celui d'ouverture.
UpdateTabButtonStates();
// L'ecran a pu etre ouvert, referme, puis rouvert apres un changement fait
// ailleurs : on relit toujours plutot que de faire confiance a l'affichage.
if (USettingsTabWidget* ActiveTab = GetActiveTab())
@@ -109,12 +142,18 @@ void USettingsMenuWidget::Open()
void USettingsMenuWidget::Close()
{
if (ResetDialog)
// Fermer l'ecran avec une question en suspens vaut un refus : sinon le
// demandeur -- typiquement un changement de resolution en attente de
// confirmation -- resterait dans un etat non tranche.
if (bResetDialogOpen)
{
ResetDialog->Close();
FinishConfirmation(false);
}
bResetDialogOpen = false;
if (bKeyCaptureOpen)
{
CloseKeyCapture();
}
SetVisibility(ESlateVisibility::Collapsed);
@@ -124,6 +163,20 @@ void USettingsMenuWidget::Close()
{
Settings->SaveSettings();
}
// Les touches remappees vivent ailleurs : Enhanced Input les serialise pour
// son compte, dans son propre fichier. Deux systemes de persistance, mais un
// seul moment ou l'on ecrit.
if (const ULocalPlayer* LocalPlayer = GetOwningLocalPlayer())
{
if (UEnhancedInputLocalPlayerSubsystem* Subsystem = LocalPlayer->GetSubsystem<UEnhancedInputLocalPlayerSubsystem>())
{
if (UEnhancedInputUserSettings* InputSettings = Subsystem->GetUserSettings())
{
InputSettings->SaveSettings();
}
}
}
}
FReply USettingsMenuWidget::NativeOnKeyDown(const FGeometry& InGeometry, const FKeyEvent& InKeyEvent)
@@ -132,12 +185,18 @@ FReply USettingsMenuWidget::NativeOnKeyDown(const FGeometry& InGeometry, const F
if (Key == EKeys::Escape)
{
// La boite de confirmation gere Echap elle-meme quand elle a le focus.
// Si elle l'a perdu, on annule ici plutot que de fermer tout l'ecran en
// laissant la question affichee.
// La boite et le voile gerent Echap eux-memes quand ils ont le focus.
// S'ils l'ont perdu, on annule ici plutot que de fermer tout l'ecran en
// laissant la question ou le voile affiche.
if (bKeyCaptureOpen)
{
CloseKeyCapture();
return FReply::Handled();
}
if (bResetDialogOpen)
{
HandleResetCancelled();
FinishConfirmation(false);
return FReply::Handled();
}
@@ -165,6 +224,9 @@ void USettingsMenuWidget::ShowTab(int32 Index)
TabSwitcher->SetActiveWidgetIndex(Index);
// L'onglet a change, donc le bouton a griser aussi.
UpdateTabButtonStates();
if (USettingsTabWidget* ActiveTab = GetActiveTab())
{
ActiveTab->RefreshFromSettings();
@@ -174,26 +236,24 @@ void USettingsMenuWidget::ShowTab(int32 Index)
void USettingsMenuWidget::UpdateTabButtonStates()
{
const int32 PageCount = TabSwitcher ? TabSwitcher->GetNumWidgets() : 0;
const int32 ActiveIndex = TabSwitcher ? TabSwitcher->GetActiveWidgetIndex() : INDEX_NONE;
if (GameplayTabButton)
// Un bouton est cliquable si sa page existe ET qu'on n'y est pas deja.
// Griser l'onglet courant lui sert d'indicateur de position : sans ca, rien
// ne distingue visuellement l'onglet affiche des autres.
auto ApplyState = [PageCount, ActiveIndex](UButton* Button, int32 TabIndex)
{
GameplayTabButton->SetIsEnabled(PageCount > TabIndexGameplay);
}
if (Button)
{
Button->SetIsEnabled(PageCount > TabIndex && ActiveIndex != TabIndex);
}
};
if (DisplayTabButton)
{
DisplayTabButton->SetIsEnabled(PageCount > TabIndexDisplay);
}
if (GraphicsTabButton)
{
GraphicsTabButton->SetIsEnabled(PageCount > TabIndexGraphics);
}
if (AudioTabButton)
{
AudioTabButton->SetIsEnabled(PageCount > TabIndexAudio);
}
ApplyState(GameplayTabButton, TabIndexGameplay);
ApplyState(DisplayTabButton, TabIndexDisplay);
ApplyState(GraphicsTabButton, TabIndexGraphics);
ApplyState(ControlsTabButton, TabIndexControls);
ApplyState(AudioTabButton, TabIndexAudio);
}
void USettingsMenuWidget::HandleBackClicked()
@@ -201,20 +261,66 @@ void USettingsMenuWidget::HandleBackClicked()
OnCloseRequested.Broadcast();
}
void USettingsMenuWidget::HandleResetClicked()
UConfirmDialogWidget* USettingsMenuWidget::EnsureDialog()
{
// Sans boite assignee on ne bloque pas le joueur : on reinitialise.
if (!ResetDialog)
if (ResetDialog)
{
HandleResetConfirmed();
return;
return ResetDialog;
}
bResetDialogOpen = true;
ResetDialog->Open(ResetDialogMessage);
if (!ResetDialogClass)
{
return nullptr;
}
ResetDialog = CreateWidget<UConfirmDialogWidget>(GetOwningPlayer(), ResetDialogClass);
if (!ResetDialog)
{
return nullptr;
}
ResetDialog->OnConfirmed.AddDynamic(this, &USettingsMenuWidget::HandleDialogConfirmed);
ResetDialog->OnCancelled.AddDynamic(this, &USettingsMenuWidget::HandleDialogCancelled);
ResetDialog->AddToViewport(DialogZOrder);
return ResetDialog;
}
void USettingsMenuWidget::HandleResetConfirmed()
bool USettingsMenuWidget::RequestConfirmation(const FText& MessageOrFormat, float CountdownSeconds, const FOnSettingsConfirmationResult& OnResult)
{
UConfirmDialogWidget* Dialog = EnsureDialog();
if (!Dialog)
{
UE_LOG(LogTemp, Warning, TEXT("USettingsMenuWidget : ResetDialogClass n'est pas assignee, la question ne peut pas etre posee."));
return false;
}
// Une seule boite pour tout l'ecran : si une question etait deja posee, la
// nouvelle l'ecrase. On repond "non" a l'ancienne pour que son demandeur ne
// reste pas suspendu a une reponse qui n'arrivera plus.
if (PendingConfirmation.IsBound())
{
const FOnSettingsConfirmationResult Superseded = PendingConfirmation;
PendingConfirmation.Unbind();
Superseded.ExecuteIfBound(false);
}
PendingConfirmation = OnResult;
bResetDialogOpen = true;
if (CountdownSeconds > 0.f)
{
Dialog->OpenWithCountdown(MessageOrFormat, CountdownSeconds);
}
else
{
Dialog->Open(MessageOrFormat);
}
return true;
}
void USettingsMenuWidget::FinishConfirmation(bool bConfirmed)
{
bResetDialogOpen = false;
@@ -223,26 +329,51 @@ void USettingsMenuWidget::HandleResetConfirmed()
ResetDialog->Close();
}
// Detache AVANT d'appeler : le demandeur a parfaitement le droit de reposer
// une question dans son propre handler.
const FOnSettingsConfirmationResult Answered = PendingConfirmation;
PendingConfirmation.Unbind();
SetKeyboardFocus();
Answered.ExecuteIfBound(bConfirmed);
}
void USettingsMenuWidget::HandleDialogConfirmed()
{
FinishConfirmation(true);
}
void USettingsMenuWidget::HandleDialogCancelled()
{
FinishConfirmation(false);
}
void USettingsMenuWidget::HandleResetClicked()
{
FOnSettingsConfirmationResult Callback;
Callback.BindDynamic(this, &USettingsMenuWidget::HandleResetAnswered);
// Sans boite configuree on ne bloque pas le joueur : on reinitialise.
if (!RequestConfirmation(ResetDialogMessage, 0.f, Callback))
{
HandleResetAnswered(true);
}
}
void USettingsMenuWidget::HandleResetAnswered(bool bConfirmed)
{
if (!bConfirmed)
{
return;
}
// Uniquement l'onglet visible : un "Reinitialiser" qui remettrait aussi la
// resolution et le volume serait une mauvaise surprise.
if (USettingsTabWidget* ActiveTab = GetActiveTab())
{
ActiveTab->ResetToDefaults();
}
SetKeyboardFocus();
}
void USettingsMenuWidget::HandleResetCancelled()
{
bResetDialogOpen = false;
if (ResetDialog)
{
ResetDialog->Close();
}
SetKeyboardFocus();
}
void USettingsMenuWidget::HandleGameplayTabClicked()
@@ -260,9 +391,127 @@ void USettingsMenuWidget::HandleGraphicsTabClicked()
ShowTab(TabIndexGraphics);
}
void USettingsMenuWidget::HandleControlsTabClicked()
{
ShowTab(TabIndexControls);
}
void USettingsMenuWidget::HandleAudioTabClicked()
{
ShowTab(TabIndexAudio);
}
bool USettingsMenuWidget::RequestKeyCapture(const FText& ActionLabel, const FOnSettingsKeyCaptureResult& OnResult)
{
if (!KeyCaptureClass)
{
UE_LOG(LogTemp, Warning, TEXT("USettingsMenuWidget : KeyCaptureClass n'est pas assignee, aucun remappage possible."));
return false;
}
// Cree au premier remappage seulement, puis reutilise -- comme la boite.
if (!KeyCapture)
{
KeyCapture = CreateWidget<UKeyCaptureWidget>(GetOwningPlayer(), KeyCaptureClass);
if (!KeyCapture)
{
return false;
}
KeyCapture->OnKeyCaptured.AddDynamic(this, &USettingsMenuWidget::HandleKeyCaptured);
KeyCapture->OnCaptureCancelled.AddDynamic(this, &USettingsMenuWidget::HandleKeyCaptureCancelled);
KeyCapture->OnConflictAnswered.AddDynamic(this, &USettingsMenuWidget::HandleKeyConflictAnswered);
// Au-dessus de la boite : une confirmation de conflit s'ouvre APRES la
// capture, mais le voile doit couvrir l'ecran pendant qu'il attend.
KeyCapture->AddToViewport(DialogZOrder + 1);
}
// Une capture deja en cours est abandonnee : son demandeur recoit une touche
// invalide plutot que de rester suspendu.
if (PendingKeyCapture.IsBound())
{
const FOnSettingsKeyCaptureResult Superseded = PendingKeyCapture;
PendingKeyCapture.Unbind();
Superseded.ExecuteIfBound(FKey());
}
PendingKeyCapture = OnResult;
bKeyCaptureOpen = true;
KeyCapture->BeginCapture(ActionLabel);
return true;
}
void USettingsMenuWidget::ShowKeyCaptureConflict(const FText& Message, const FOnSettingsConfirmationResult& OnAnswer)
{
if (!KeyCapture || !bKeyCaptureOpen)
{
// Le voile a ete referme entre-temps : on repond "non" plutot que de
// laisser le demandeur suspendu.
OnAnswer.ExecuteIfBound(false);
return;
}
PendingConflictAnswer = OnAnswer;
KeyCapture->ShowConflict(Message);
}
void USettingsMenuWidget::CloseKeyCapture()
{
bKeyCaptureOpen = false;
if (KeyCapture)
{
KeyCapture->EndCapture();
}
// On detache tout : si quelqu'un attendait encore une reponse, il la recoit
// negative plutot que jamais.
const FOnSettingsKeyCaptureResult AbandonedCapture = PendingKeyCapture;
const FOnSettingsConfirmationResult AbandonedConflict = PendingConflictAnswer;
PendingKeyCapture.Unbind();
PendingConflictAnswer.Unbind();
SetKeyboardFocus();
AbandonedCapture.ExecuteIfBound(FKey());
AbandonedConflict.ExecuteIfBound(false);
}
void USettingsMenuWidget::HandleKeyCaptured(FKey CapturedKey)
{
// Le voile RESTE ouvert : c'est l'onglet qui decide s'il y a conflit, et
// donc s'il faut le basculer sur la question ou le refermer.
const FOnSettingsKeyCaptureResult Answered = PendingKeyCapture;
PendingKeyCapture.Unbind();
Answered.ExecuteIfBound(CapturedKey);
}
void USettingsMenuWidget::HandleKeyCaptureCancelled()
{
// Echap pendant la capture : rien a arbitrer, on referme.
CloseKeyCapture();
}
void USettingsMenuWidget::HandleKeyConflictAnswered(bool bConfirmed)
{
// Detache AVANT d'appeler : le demandeur applique le remappage dans son
// handler et pourrait vouloir rouvrir une capture derriere.
const FOnSettingsConfirmationResult Answered = PendingConflictAnswer;
PendingConflictAnswer.Unbind();
bKeyCaptureOpen = false;
if (KeyCapture)
{
KeyCapture->EndCapture();
}
SetKeyboardFocus();
Answered.ExecuteIfBound(bConfirmed);
}
#undef LOCTEXT_NAMESPACE