// Fill out your copyright notice in the Description page of Project Settings. #include "SettingRowWidget.h" #include "Components/Button.h" #include "Components/ProgressBar.h" #include "Components/Slider.h" #include "Components/TextBlock.h" void USettingRowWidget::NativeOnInitialized() { Super::NativeOnInitialized(); // NativeOnInitialized et non NativeConstruct : l'onglet parent appelle // SetupAs... depuis SON NativeConstruct, et rien ne garantit l'ordre entre // le construct du parent et celui de ses enfants. Initialize, lui, tourne // une seule fois a la creation de l'arbre, avant tout le monde. if (LeftArrowButton) { LeftArrowButton->OnClicked.AddDynamic(this, &USettingRowWidget::HandleLeftClicked); } if (RightArrowButton) { RightArrowButton->OnClicked.AddDynamic(this, &USettingRowWidget::HandleRightClicked); } if (ValueSlider) { ValueSlider->OnValueChanged.AddDynamic(this, &USettingRowWidget::HandleSliderValueChanged); } // La valeur est POSEE SUR le curseur : si elle restait cliquable, elle // avalerait le clic au milieu de la course et le glissement ne partirait // jamais. Force ici plutot que dans le WBP : c'est le genre de case qu'on // oublie et dont le symptome (une zone morte au centre) est illisible. if (ValueText) { ValueText->SetVisibility(ESlateVisibility::HitTestInvisible); } } void USettingRowWidget::SetupAsChoice(const FText& Label, const TArray& Options, int32 Index) { bNumeric = false; ChoiceLabels = Options; StepCount = FMath::Max(Options.Num(), 1); ArrowIndexDelta = 1; MaxSelectableIndex = INDEX_NONE; CurrentIndex = FMath::Clamp(Index, 0, StepCount - 1); if (LabelText) { LabelText->SetText(Label); } // Une jauge sur "Maintien / Bascule" ne veut rien dire : deux positions et // rien a doser. On replie l'empilement plutot que d'imposer un second WBP. if (ValueBar) { ValueBar->SetVisibility(ESlateVisibility::Collapsed); } if (ValueSlider) { ValueSlider->SetVisibility(ESlateVisibility::Collapsed); } Refresh(); } void USettingRowWidget::SetupAsNumber(const FText& Label, float Min, float Max, float Step, float ArrowStep, float Value, const FText& Suffix) { bNumeric = true; ChoiceLabels.Reset(); NumericMin = Min; NumericStep = FMath::Max(Step, KINDA_SMALL_NUMBER); NumericSuffix = Suffix; // +1 : les deux extremites comptent. 70 a 120 par 1 fait 51 paliers, pas 50. StepCount = FMath::Max(FMath::FloorToInt((Max - Min) / NumericStep) + 1, 1); // Le bond des fleches ne vit qu'en nombre de paliers fins : tout le reste du // widget raisonne en index, y compris le curseur. ArrowIndexDelta = FMath::Max(FMath::RoundToInt(ArrowStep / NumericStep), 1); MaxSelectableIndex = INDEX_NONE; if (LabelText) { LabelText->SetText(Label); } // HitTestInvisible : le remplissage est un decor pose SOUS le curseur, il ne // doit surtout pas intercepter le clic qui devrait lancer le glissement. if (ValueBar) { ValueBar->SetVisibility(ESlateVisibility::HitTestInvisible); } if (ValueSlider) { ValueSlider->SetVisibility(ESlateVisibility::Visible); // La course du curseur est graduee en INDEX, pas dans l'unite du // reglage : 0 a 10 pour un FOV de 70 a 120 par pas de 5. Le glissement // ne peut donc produire qu'un palier valide, et le meme curseur sert a // n'importe quelle plage sans conversion. ValueSlider->SetMinValue(0.f); ValueSlider->SetMaxValue(static_cast(FMath::Max(StepCount - 1, 1))); ValueSlider->SetStepSize(1.f); } SetNumericValueSilently(Value); } float USettingRowWidget::GetNumericValue() const { return NumericMin + CurrentIndex * NumericStep; } void USettingRowWidget::SetIndexSilently(int32 NewIndex) { // Borne sur StepCount et non sur MaxSelectable : le code a le droit // d'afficher un etat que le joueur ne peut pas choisir. CurrentIndex = FMath::Clamp(NewIndex, 0, StepCount - 1); Refresh(); } void USettingRowWidget::SetMaxSelectableIndex(int32 InMaxSelectableIndex) { MaxSelectableIndex = InMaxSelectableIndex; Refresh(); } int32 USettingRowWidget::MaxSelectable() const { return MaxSelectableIndex == INDEX_NONE ? StepCount - 1 : FMath::Clamp(MaxSelectableIndex, 0, StepCount - 1); } void USettingRowWidget::SetNumericValueSilently(float Value) { SetIndexSilently(FMath::RoundToInt((Value - NumericMin) / NumericStep)); } void USettingRowWidget::HandleLeftClicked() { StepIndex(ArrowTargetIndex(-1) - CurrentIndex); } void USettingRowWidget::HandleRightClicked() { StepIndex(ArrowTargetIndex(1) - CurrentIndex); } int32 USettingRowWidget::ArrowTargetIndex(int32 Direction) const { // Pas de bond a arrondir : une ligne de choix, ou un reglage dont les deux // pas sont identiques. if (ArrowIndexDelta <= 1) { return CurrentIndex + Direction; } // On vise le multiple du bond suivant, pas CurrentIndex + bond : arrive a 37 // au curseur, la fleche droite doit donner 40 et la gauche 35. Sinon les // fleches perdent leur reperage des qu'on a glisse une fois. if (Direction > 0) { return ((CurrentIndex / ArrowIndexDelta) + 1) * ArrowIndexDelta; } // Division entiere arrondie au superieur, pour que 35 redescende bien a 30 // et pas a 35 (ce qui n'aurait rien fait). return ((CurrentIndex + ArrowIndexDelta - 1) / ArrowIndexDelta - 1) * ArrowIndexDelta; } void USettingRowWidget::HandleSliderValueChanged(float NewValue) { if (bRefreshingSlider) { return; } // On passe par StepIndex plutot que d'ecrire CurrentIndex : le glissement // et les fleches doivent produire exactement le meme effet, y compris le // grisage des fleches et la diffusion du delegue. StepIndex(FMath::RoundToInt(NewValue) - CurrentIndex); // Puis on recale la poignee sur le cran, meme si l'index n'a pas bouge : // StepIndex sort sans rien faire dans ce cas, et la poignee resterait // plantee entre deux paliers. Refresh(); } void USettingRowWidget::StepIndex(int32 Delta) { const int32 NewIndex = FMath::Clamp(CurrentIndex + Delta, 0, MaxSelectable()); // Pas de bouclage, volontairement : passer de 100 a 5 de sensibilite d'un // clic de trop rendrait le jeu injouable sans qu'on comprenne pourquoi. if (NewIndex == CurrentIndex) { return; } CurrentIndex = NewIndex; Refresh(); OnIndexChanged.Broadcast(CurrentIndex); } void USettingRowWidget::Refresh() { if (ValueText) { if (bNumeric) { FNumberFormattingOptions Format; Format.MaximumFractionalDigits = 0; const FText Number = FText::AsNumber(GetNumericValue(), &Format); // FormatOrdered plutot qu'une concatenation : selon la langue, l'unite // ne se colle pas forcement du meme cote ni avec la meme espace. ValueText->SetText(NumericSuffix.IsEmpty() ? Number : FText::FormatOrdered(NSLOCTEXT("Reglages", "ValeurAvecUnite", "{0}{1}"), Number, NumericSuffix)); } else if (ChoiceLabels.IsValidIndex(CurrentIndex)) { ValueText->SetText(ChoiceLabels[CurrentIndex]); } } if (ValueBar) { // Un seul palier : la jauge n'a aucun sens, on la remplit entierement // plutot que de diviser par zero. ValueBar->SetPercent(StepCount > 1 ? static_cast(CurrentIndex) / static_cast(StepCount - 1) : 1.f); } if (ValueSlider && bNumeric) { // Le drapeau evite qu'un repositionnement venant du code soit relu comme // un geste du joueur. bRefreshingSlider = true; ValueSlider->SetValue(static_cast(CurrentIndex)); bRefreshingSlider = false; } // Les fleches se grisent aux extremites : le joueur voit qu'il est au bout // au lieu de cliquer dans le vide. if (LeftArrowButton) { LeftArrowButton->SetIsEnabled(CurrentIndex > 0); } if (RightArrowButton) { RightArrowButton->SetIsEnabled(CurrentIndex < MaxSelectable()); } }