// 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(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(); return Subsystem ? Subsystem->GetUserSettings() : nullptr; } TArray USettingsControlsWidget::GetAllRows() const { // L'ordre est celui de l'affichage : il sert aussi a la reinitialisation. TArray Rows; Rows.Reserve(11); for (USettingKeyRowWidget* Row : { MoveForwardRow.Get(), MoveBackwardRow.Get(), MoveLeftRow.Get(), MoveRightRow.Get(), JumpRow.Get(), SprintRow.Get(), CrouchRow.Get(), InteractRow.Get(), UseItemRow.Get(), ToggleInventoryRow.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()) { 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& 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(); 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 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