// Fill out your copyright notice in the Description page of Project Settings. #include "SettingsMenuWidget.h" #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" #define LOCTEXT_NAMESPACE "Reglages" namespace { // Ordre des pages dans le WidgetSwitcher. Les trois dernieres n'existent pas // encore : leur bouton se grise tout seul tant que la page est absente. constexpr int32 TabIndexGameplay = 0; constexpr int32 TabIndexDisplay = 1; constexpr int32 TabIndexGraphics = 2; constexpr int32 TabIndexControls = 3; constexpr int32 TabIndexAudio = 4; } USettingsMenuWidget::USettingsMenuWidget(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) { // Sans ce drapeau le widget ne peut pas prendre le focus clavier, et // NativeOnKeyDown ne serait jamais appele. Il ne se change qu'a la // construction, d'ou sa presence ici et pas dans Open(). SetIsFocusable(true); ResetDialogMessage = LOCTEXT("ResetConfirm", "Réinitialiser cet onglet ?"); } void USettingsMenuWidget::NativeConstruct() { Super::NativeConstruct(); if (BackButton) { BackButton->OnClicked.AddDynamic(this, &USettingsMenuWidget::HandleBackClicked); } if (ResetButton) { ResetButton->OnClicked.AddDynamic(this, &USettingsMenuWidget::HandleResetClicked); } if (GameplayTabButton) { GameplayTabButton->OnClicked.AddDynamic(this, &USettingsMenuWidget::HandleGameplayTabClicked); } if (DisplayTabButton) { DisplayTabButton->OnClicked.AddDynamic(this, &USettingsMenuWidget::HandleDisplayTabClicked); } if (GraphicsTabButton) { GraphicsTabButton->OnClicked.AddDynamic(this, &USettingsMenuWidget::HandleGraphicsTabClicked); } if (ControlsTabButton) { ControlsTabButton->OnClicked.AddDynamic(this, &USettingsMenuWidget::HandleControlsTabClicked); } if (AudioTabButton) { AudioTabButton->OnClicked.AddDynamic(this, &USettingsMenuWidget::HandleAudioTabClicked); } // 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) { for (int32 Index = 0; Index < TabSwitcher->GetNumWidgets(); ++Index) { if (USettingsTabWidget* Tab = Cast(TabSwitcher->GetWidgetAtIndex(Index))) { Tab->SetOwningMenu(this); } } } UpdateTabButtonStates(); } void USettingsMenuWidget::NativeDestruct() { if (BackButton) { BackButton->OnClicked.RemoveDynamic(this, &USettingsMenuWidget::HandleBackClicked); } if (ResetButton) { 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(); } 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()) { ActiveTab->RefreshFromSettings(); } // Le focus doit etre repris a CHAQUE ouverture, sinon Echap reste sans effet. SetKeyboardFocus(); } void USettingsMenuWidget::Close() { // 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) { FinishConfirmation(false); } if (bKeyCaptureOpen) { CloseKeyCapture(); } SetVisibility(ESlateVisibility::Collapsed); // La sauvegarde a lieu ici et pas a chaque clic de fleche : ecrire le .ini // vingt fois pendant qu'on cherche la bonne sensibilite ne sert a rien. if (USurvivalUserSettings* Settings = USurvivalUserSettings::Get()) { 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()) { if (UEnhancedInputUserSettings* InputSettings = Subsystem->GetUserSettings()) { InputSettings->SaveSettings(); } } } } FReply USettingsMenuWidget::NativeOnKeyDown(const FGeometry& InGeometry, const FKeyEvent& InKeyEvent) { const FKey Key = InKeyEvent.GetKey(); if (Key == EKeys::Escape) { // 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) { FinishConfirmation(false); return FReply::Handled(); } OnCloseRequested.Broadcast(); // Handled : sans ca la touche continue de remonter et le menu de pause // la consommerait une seconde fois. return FReply::Handled(); } return Super::NativeOnKeyDown(InGeometry, InKeyEvent); } USettingsTabWidget* USettingsMenuWidget::GetActiveTab() const { return TabSwitcher ? Cast(TabSwitcher->GetActiveWidget()) : nullptr; } void USettingsMenuWidget::ShowTab(int32 Index) { if (!TabSwitcher || Index < 0 || Index >= TabSwitcher->GetNumWidgets()) { return; } TabSwitcher->SetActiveWidgetIndex(Index); // L'onglet a change, donc le bouton a griser aussi. UpdateTabButtonStates(); if (USettingsTabWidget* ActiveTab = GetActiveTab()) { ActiveTab->RefreshFromSettings(); } } void USettingsMenuWidget::UpdateTabButtonStates() { const int32 PageCount = TabSwitcher ? TabSwitcher->GetNumWidgets() : 0; const int32 ActiveIndex = TabSwitcher ? TabSwitcher->GetActiveWidgetIndex() : INDEX_NONE; // 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) { if (Button) { Button->SetIsEnabled(PageCount > TabIndex && ActiveIndex != TabIndex); } }; ApplyState(GameplayTabButton, TabIndexGameplay); ApplyState(DisplayTabButton, TabIndexDisplay); ApplyState(GraphicsTabButton, TabIndexGraphics); ApplyState(ControlsTabButton, TabIndexControls); ApplyState(AudioTabButton, TabIndexAudio); } void USettingsMenuWidget::HandleBackClicked() { OnCloseRequested.Broadcast(); } UConfirmDialogWidget* USettingsMenuWidget::EnsureDialog() { if (ResetDialog) { return ResetDialog; } if (!ResetDialogClass) { return nullptr; } ResetDialog = CreateWidget(GetOwningPlayer(), ResetDialogClass); if (!ResetDialog) { return nullptr; } ResetDialog->OnConfirmed.AddDynamic(this, &USettingsMenuWidget::HandleDialogConfirmed); ResetDialog->OnCancelled.AddDynamic(this, &USettingsMenuWidget::HandleDialogCancelled); ResetDialog->AddToViewport(DialogZOrder); return ResetDialog; } 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; if (ResetDialog) { 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(); } } void USettingsMenuWidget::HandleGameplayTabClicked() { ShowTab(TabIndexGameplay); } void USettingsMenuWidget::HandleDisplayTabClicked() { ShowTab(TabIndexDisplay); } 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(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