Files

268 lines
7.5 KiB
C++

// 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<FText>& 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<float>(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<float>(CurrentIndex) / static_cast<float>(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<float>(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());
}
}