Files
Unreal_EmberWild/Source/Survival_projet/Private/SettingsControlsWidget.cpp
T
2026-08-06 13:27:21 +02:00

356 lines
11 KiB
C++

// Fill out your copyright notice in the Description page of Project Settings.
#include "SettingsControlsWidget.h"
#include "EnhancedInputSubsystems.h"
#include "Engine/LocalPlayer.h"
#include "GameFramework/PlayerController.h"
#include "SettingKeyRowWidget.h"
#include "SettingsMenuWidget.h"
#include "SurvivalGameInstance.h"
#include "UserSettings/EnhancedInputUserSettings.h"
#define LOCTEXT_NAMESPACE "Reglages"
USettingsControlsWidget::USettingsControlsWidget(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
// Dans le CONSTRUCTEUR et non dans NativeConstruct : ce dernier tourne a
// chaque ouverture et ecraserait le texte que tu aurais regle dans les Class
// Defaults de WBP_SettingsControls, rendant la UPROPERTY inutile.
//
// Trois informations, pas deux : la touche, qui la possede aujourd'hui, et a
// qui on la donne. Sans la troisieme, un joueur qui a ouvert le voile puis
// hesite ne sait plus quelle ligne il etait en train de modifier.
ConflictMessage = LOCTEXT("ConflitTouche",
"« {0} » est déjà utilisée par « {1} ».\nL'assigner à « {2} » ?\n\n« {1} » n'aura plus de touche.");
}
void USettingsControlsWidget::NativeConstruct()
{
Super::NativeConstruct();
// Les identifiants doivent correspondre EXACTEMENT au champ "Name" des
// Player Mappable Key Settings de IMC_Default. Une faute de frappe donne une
// ligne perpetuellement "Non assignée" -- c'est le seul point de contact
// entre le C++ et l'editeur, et il n'est verifiable qu'au runtime.
SetupRow(MoveForwardRow, TEXT("MoveForward"), LOCTEXT("Avancer", "Avancer"));
SetupRow(MoveBackwardRow, TEXT("MoveBackward"), LOCTEXT("Reculer", "Reculer"));
SetupRow(MoveLeftRow, TEXT("MoveLeft"), LOCTEXT("AllerGauche", "Aller à gauche"));
SetupRow(MoveRightRow, TEXT("MoveRight"), LOCTEXT("AllerDroite", "Aller à droite"));
SetupRow(JumpRow, TEXT("Jump"), LOCTEXT("Sauter", "Sauter"));
SetupRow(SprintRow, TEXT("Sprint"), LOCTEXT("Sprinter", "Sprinter"));
SetupRow(CrouchRow, TEXT("Crouch"), LOCTEXT("SAccroupir", "S'accroupir"));
SetupRow(InteractRow, TEXT("Interact"), LOCTEXT("Interagir", "Interagir"));
SetupRow(UseItemRow, TEXT("UseItem"), LOCTEXT("UtiliserObjet", "Utiliser l'objet"));
SetupRow(ToggleInventoryRow, TEXT("ToggleInventory"), LOCTEXT("Inventaire", "Inventaire"));
SetupRow(ToggleCraftingRow, TEXT("ToggleCrafting"), LOCTEXT("Fabrication", "Fabrication"));
SetupRow(DropItemRow, TEXT("DropItem"), LOCTEXT("Jeter", "Jeter"));
RefreshFromSettings();
}
void USettingsControlsWidget::SetupRow(USettingKeyRowWidget* Row, FName MappingName, const FText& Label)
{
if (!Row)
{
return;
}
MappingLabels.Add(MappingName, Label);
// Touche invalide au depart : RefreshFromSettings ira chercher la vraie juste
// apres. Poser une valeur en dur ici mentirait le temps d'une frame.
Row->Setup(MappingName, Label, FKey());
// Un seul handler pour les onze lignes : chacune renvoie son propre nom de
// mapping, il n'y a rien a distinguer ici.
Row->OnRebindRequested.AddDynamic(this, &USettingsControlsWidget::HandleRebindRequested);
}
UEnhancedInputUserSettings* USettingsControlsWidget::GetInputSettings() const
{
const ULocalPlayer* LocalPlayer = GetOwningLocalPlayer();
if (!LocalPlayer)
{
return nullptr;
}
UEnhancedInputLocalPlayerSubsystem* Subsystem = LocalPlayer->GetSubsystem<UEnhancedInputLocalPlayerSubsystem>();
return Subsystem ? Subsystem->GetUserSettings() : nullptr;
}
TArray<USettingKeyRowWidget*> USettingsControlsWidget::GetAllRows() const
{
// L'ordre est celui de l'affichage : il sert aussi a la reinitialisation.
TArray<USettingKeyRowWidget*> Rows;
Rows.Reserve(12);
for (USettingKeyRowWidget* Row : { MoveForwardRow.Get(), MoveBackwardRow.Get(), MoveLeftRow.Get(),
MoveRightRow.Get(), JumpRow.Get(), SprintRow.Get(), CrouchRow.Get(), InteractRow.Get(),
UseItemRow.Get(), ToggleInventoryRow.Get(), ToggleCraftingRow.Get(), DropItemRow.Get() })
{
if (Row)
{
Rows.Add(Row);
}
}
return Rows;
}
USettingKeyRowWidget* USettingsControlsWidget::FindRow(FName MappingName) const
{
for (USettingKeyRowWidget* Row : GetAllRows())
{
if (Row->GetMappingName() == MappingName)
{
return Row;
}
}
return nullptr;
}
FText USettingsControlsWidget::GetMappingDisplayName(FName MappingName) const
{
if (const FText* Label = MappingLabels.Find(MappingName))
{
return *Label;
}
// Repli sur l'identifiant technique : moins joli, mais toujours plus utile
// qu'un libelle vide dans un message de conflit.
return FText::FromName(MappingName);
}
void USettingsControlsWidget::RefreshFromSettings()
{
// On declare les contextes JUSTE AVANT de lire : c'est le seul moment ou
// l'on est certain que le joueur local a son PlayerController, donc que le
// sous-systeme a cree ses UserSettings. Tout enregistrement plus precoce
// tombe dans le vide -- ULocalPlayer previent ses abonnes avant ses
// sous-systemes. L'appel est idempotent, le repeter ne coute rien.
if (USurvivalGameInstance* GameInstance = GetGameInstance<USurvivalGameInstance>())
{
GameInstance->RegisterMappableContexts();
}
const UEnhancedInputUserSettings* Settings = GetInputSettings();
const UEnhancedPlayerMappableKeyProfile* Profile = Settings ? Settings->GetActiveKeyProfile() : nullptr;
if (!Profile)
{
UE_LOG(LogTemp, Warning, TEXT("USettingsControlsWidget : aucun profil de touches. Verifie 'Enable User Settings' dans Project Settings > Enhanced Input."));
return;
}
// On parcourt les lignes DECLAREES et on va chercher leur mapping, plutot que
// l'inverse : le profil contient aussi des mappings qu'on n'expose pas, et
// l'ordre d'affichage est le notre, pas le sien.
for (const TPair<FName, FKeyMappingRow>& Pair : Profile->GetPlayerMappingRows())
{
USettingKeyRowWidget* Row = FindRow(Pair.Key);
if (!Row)
{
continue;
}
FKey CurrentKey;
for (const FPlayerKeyMapping& Mapping : Pair.Value.Mappings)
{
CurrentKey = Mapping.GetCurrentKey();
break;
}
Row->SetKey(CurrentKey);
}
}
void USettingsControlsWidget::ResetToDefaults()
{
UEnhancedInputUserSettings* Settings = GetInputSettings();
if (!Settings)
{
return;
}
for (const USettingKeyRowWidget* Row : GetAllRows())
{
FMapPlayerKeyArgs Args;
Args.MappingName = Row->GetMappingName();
FGameplayTagContainer FailureReason;
Settings->ResetAllPlayerKeysInRow(Args, FailureReason);
}
Settings->ApplySettings();
// Reinitialiser rend les touches de l'IMC, donc du QWERTY : sur un clavier
// AZERTY le joueur se retrouverait avec un WASD qu'il ne peut pas atteindre.
// On repose donc la disposition juste apres. "Par defaut" veut dire "par defaut
// POUR TON CLAVIER", pas "par defaut pour le clavier du developpeur".
if (USurvivalGameInstance* GameInstance = GetGameInstance<USurvivalGameInstance>())
{
GameInstance->ApplyKeyboardLayoutToDefaultKeys();
}
RefreshFromSettings();
}
void USettingsControlsWidget::HandleRebindRequested(FName MappingName)
{
if (!OwningMenu.IsValid())
{
return;
}
PendingMappingName = MappingName;
PendingKey = FKey();
ConflictingMappingName = NAME_None;
FOnSettingsKeyCaptureResult Callback;
Callback.BindDynamic(this, &USettingsControlsWidget::HandleKeyCaptured);
OwningMenu->RequestKeyCapture(GetMappingDisplayName(MappingName), Callback);
}
void USettingsControlsWidget::HandleKeyCaptured(FKey CapturedKey)
{
UEnhancedInputUserSettings* Settings = GetInputSettings();
// Touche invalide = le joueur a annule avec Echap.
if (!CapturedKey.IsValid() || PendingMappingName.IsNone() || !Settings)
{
PendingMappingName = NAME_None;
return;
}
PendingKey = CapturedKey;
const UEnhancedPlayerMappableKeyProfile* Profile = Settings->GetActiveKeyProfile();
if (!Profile)
{
return;
}
// Le moteur sait deja repondre a "qui utilise cette touche ?" : pas de
// comparaison a la main sur toutes les lignes.
TArray<FName> Owners;
Profile->GetMappingNamesForKey(CapturedKey, Owners);
ConflictingMappingName = NAME_None;
for (const FName& Owner : Owners)
{
// Se voler sa propre touche n'a rien d'un conflit.
if (Owner != PendingMappingName && FindRow(Owner))
{
ConflictingMappingName = Owner;
break;
}
}
if (ConflictingMappingName.IsNone())
{
// Rien a arbitrer : on applique et on referme le voile.
ApplyPendingRebind();
if (OwningMenu.IsValid())
{
OwningMenu->CloseKeyCapture();
}
return;
}
if (!OwningMenu.IsValid())
{
return;
}
FOnSettingsConfirmationResult Callback;
Callback.BindDynamic(this, &USettingsControlsWidget::HandleConflictAnswered);
const FText Message = FText::Format(ConflictMessage,
PendingKey.GetDisplayName(),
GetMappingDisplayName(ConflictingMappingName),
GetMappingDisplayName(PendingMappingName));
// Le voile reste ouvert et change simplement d'etat : pour le joueur c'est
// le meme geste qui continue, pas un second ecran qui apparait.
OwningMenu->ShowKeyCaptureConflict(Message, Callback);
}
void USettingsControlsWidget::HandleConflictAnswered(bool bConfirmed)
{
// Le voile s'est deja referme de lui-meme en repondant : il n'y a plus qu'a
// appliquer, ou a oublier ce qu'on avait mis de cote.
if (!bConfirmed)
{
PendingMappingName = NAME_None;
ConflictingMappingName = NAME_None;
PendingKey = FKey();
return;
}
ApplyPendingRebind();
}
void USettingsControlsWidget::ApplyPendingRebind()
{
UEnhancedInputUserSettings* Settings = GetInputSettings();
if (!Settings || PendingMappingName.IsNone() || !PendingKey.IsValid())
{
return;
}
FGameplayTagContainer FailureReason;
// L'ancien proprietaire perd sa touche AVANT qu'on assigne la notre : deux
// actions sur la meme touche laisseraient un doublon actif, et le joueur
// declencherait les deux d'un seul appui.
//
// On lui assigne une touche VIDE au lieu d'appeler UnMapPlayerKey : ce
// dernier fait un ResetToDefault(), donc il rendrait a l'action sa touche
// d'origine -- exactement celle qu'on est en train de lui prendre. Le nom
// trompe, il annule la surcharge du joueur, il ne libere pas la touche.
if (!ConflictingMappingName.IsNone())
{
FMapPlayerKeyArgs ClearArgs;
ClearArgs.MappingName = ConflictingMappingName;
ClearArgs.Slot = EPlayerMappableKeySlot::First;
ClearArgs.NewKey = FKey();
Settings->MapPlayerKey(ClearArgs, FailureReason);
}
FMapPlayerKeyArgs MapArgs;
MapArgs.MappingName = PendingMappingName;
MapArgs.Slot = EPlayerMappableKeySlot::First;
MapArgs.NewKey = PendingKey;
Settings->MapPlayerKey(MapArgs, FailureReason);
if (!FailureReason.IsEmpty())
{
UE_LOG(LogTemp, Warning, TEXT("USettingsControlsWidget : remappage de '%s' refuse (%s)."),
*PendingMappingName.ToString(), *FailureReason.ToString());
}
// ApplySettings pousse les surcharges dans les mapping contexts deja
// enregistres : le pawn n'a rien a rebrancher, la touche est active tout de
// suite. La sauvegarde, elle, attend la fermeture de l'ecran.
Settings->ApplySettings();
PendingMappingName = NAME_None;
ConflictingMappingName = NAME_None;
PendingKey = FKey();
// Les deux lignes concernees ont bouge : on relit tout plutot que de tenir
// une comptabilite qui finirait par diverger.
RefreshFromSettings();
}
#undef LOCTEXT_NAMESPACE