518 lines
13 KiB
C++
518 lines
13 KiB
C++
// 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. Un bouton dont la page manque se
|
|
// grise tout seul, ce qui reste vrai si un onglet vient a etre retire.
|
|
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<USettingsTabWidget>(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<UEnhancedInputLocalPlayerSubsystem>())
|
|
{
|
|
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<USettingsTabWidget>(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<UConfirmDialogWidget>(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<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
|