LifePlay-Revival/Source/LifePlay/LPReadableTextSubsystem.cpp

259 lines
8 KiB
C++
Raw Permalink Normal View History

// Lot LISIB - textes lisibles hors du panneau Settings. Voir LPReadableTextSubsystem.h.
#include "LPReadableTextSubsystem.h"
#include "Blueprint/UserWidget.h"
#include "Components/EditableTextBox.h"
#include "Components/PanelWidget.h"
#include "Components/SpinBox.h"
#include "Engine/GameInstance.h"
#include "LPWidgetStyleLibrary.h"
#include "LifePlay.h"
#include "Styling/CoreStyle.h"
#include "Styling/SlateTypes.h"
#include "UObject/UObjectIterator.h"
namespace LPReadableText
{
// Intervalle entre deux passes : les menus crees entre-temps prennent le style au plus 0,25 s apres leur creation.
constexpr float PassInterval = 0.25f;
// Tailles minimales approuvees par l'utilisateur (LISIB-SPEC Q4) : les tailles plus grandes de l'asset sont gardees.
constexpr float MinSpinFontSize = 16.f;
constexpr float MinTextBoxFontSize = 14.f;
void TintBrush(FSlateBrush& Brush, const FLinearColor& Tint)
{
Brush.TintColor = FSlateColor(Tint);
}
// Comme ULPSettingsSubsystem (LPSettingsSubsystem.cpp, HardenSettingsControls), plus le remplissage au repos : le
// moteur le peint en gris clair quand la SpinBox a une plage (SSpinBox.cpp:240-241), cas des lignes de morphes.
FSpinBoxStyle MakeSpinStyle()
{
FSpinBoxStyle SpinStyle = FCoreStyle::Get().GetWidgetStyle<FSpinBoxStyle>("SpinBox");
TintBrush(SpinStyle.BackgroundBrush, FLinearColor(0.06f, 0.06f, 0.06f, 0.95f));
TintBrush(SpinStyle.HoveredBackgroundBrush, FLinearColor(0.12f, 0.12f, 0.12f, 0.98f));
TintBrush(SpinStyle.ActiveBackgroundBrush, FLinearColor(0.16f, 0.16f, 0.16f, 1.f));
TintBrush(SpinStyle.ActiveFillBrush, FLinearColor(0.2f, 0.45f, 0.9f, 0.85f));
TintBrush(SpinStyle.InactiveFillBrush, FLinearColor(0.1f, 0.22f, 0.45f, 0.9f));
// Le style de base (FCoreStyle::Get() = FAppStyle, Starship) fixe deja un remplissage de survol gris
// (StarshipCoreStyle.cpp:620) : on le teinte comme le remplissage au repos.
TintBrush(SpinStyle.HoveredFillBrush, FLinearColor(0.1f, 0.22f, 0.45f, 0.9f));
SpinStyle.ForegroundColor = FSlateColor(FLinearColor(1.f, 1.f, 1.f, 1.f));
return SpinStyle;
}
void MakeSpinReadable(USpinBox* Spin, const FSpinBoxStyle& SpinStyle)
{
// USpinBox::SetWidgetStyle recopie dans son membre et Slate pointe sur ce membre (SpinBox.cpp:111, :149-154) :
// pas de pointeur vers une temporaire, contrairement a la Text Box (ERR-088).
Spin->SetWidgetStyle(SpinStyle);
Spin->SetForegroundColor(FSlateColor(FLinearColor(1.f, 1.f, 1.f, 1.f)));
FSlateFontInfo Font = Spin->GetFont();
Font.OutlineSettings.OutlineSize = 2;
Font.OutlineSettings.bApplyOutlineToDropShadows = true;
Font.OutlineSettings.OutlineColor = FLinearColor(0.f, 0.f, 0.f, 0.95f);
if (Font.Size < MinSpinFontSize)
{
Font.Size = MinSpinFontSize;
}
Spin->SetFont(Font);
if (Spin->GetRenderOpacity() < 1.f)
{
Spin->SetRenderOpacity(1.f);
}
}
// Pas et decimales demandes par l'utilisateur pendant le PIE LISIB-J1 (2026-10-10) : l'asset ne fixe aucun pas
// (Delta 0) et la SpinBox affiche jusqu'a 6 decimales (ex. 1,054199). Pose en jeu, sans modifier l'asset (gel).
struct FSpinStep
{
const TCHAR* Menu;
const TCHAR* Widget;
float Delta;
int32 MaxFractionalDigits;
};
const FSpinStep SpinSteps[] = {
{ TEXT("CharGen"), TEXT("MoanPitch"), 0.01f, 2 }, // Sex Moan Pitch Multiplier
{ TEXT("CharGen"), TEXT("VarianceSpinBox"), 0.1f, 1 }, // Save Preset, Variance
{ TEXT("StatMenu"), TEXT("VarianceSpinBox"), 0.1f, 1 },
};
void ApplySpinStep(USpinBox* Spin, const FString& MenuName)
{
for (const FSpinStep& Step : SpinSteps)
{
if (MenuName == Step.Menu && Spin->GetFName() == Step.Widget)
{
Spin->SetDelta(Step.Delta);
Spin->SetMaxFractionalDigits(Step.MaxFractionalDigits);
return;
}
}
}
// Vrai si la Text Box a ete modifiee. Seules les Text Box sans couleur de texte fixee changent de couleur ; celles
// dont l'asset fixe une couleur (CharGen.FilenameEntry depuis STE-TXT) la gardent.
bool MakeTextBoxReadable(UEditableTextBox* Box)
{
FEditableTextBoxStyle Style = Box->GetWidgetStyle();
bool bChanged = false;
if (!Style.TextStyle.ColorAndOpacity.IsColorSpecified())
{
Style.TextStyle.ColorAndOpacity = FSlateColor(FLinearColor(0.f, 0.f, 0.f, 1.f));
bChanged = true;
}
if (Style.TextStyle.Font.Size < MinTextBoxFontSize)
{
Style.TextStyle.Font.Size = MinTextBoxFontSize;
bChanged = true;
}
if (bChanged)
{
ULPWidgetStyleLibrary::SetEditableTextBoxStyle(Box, Style);
}
return bChanged;
}
// Vrai si le widget est sous la liste des reglages (SettingsScrollBox de MainMenu), y compris a travers les
// widgets utilisateur imbriques (lignes SettingSlider, SettingCheckbox).
bool IsUnderSettings(const UWidget* Widget)
{
const UWidget* Current = Widget;
for (int32 Depth = 0; Current && Depth < 64; ++Depth)
{
if (Current->GetFName() == TEXT("SettingsScrollBox"))
{
return true;
}
if (const UPanelWidget* Parent = Current->GetParent())
{
Current = Parent;
}
else
{
// Racine d'un arbre de widgets : on remonte au widget utilisateur qui le possede.
Current = Current->GetTypedOuter<UUserWidget>();
}
}
return false;
}
// Nom du menu proprietaire, sans le suffixe _C (ex. "CharGen").
FString MenuNameOf(const UWidget* Widget)
{
const UUserWidget* Owner = Widget->GetTypedOuter<UUserWidget>();
FString OwnerName = Owner ? Owner->GetClass()->GetName() : TEXT("?");
OwnerName.RemoveFromEnd(TEXT("_C"));
return OwnerName;
}
FString DescribeWidget(const UWidget* Widget)
{
return MenuNameOf(Widget) + TEXT(".") + Widget->GetName();
}
}
void ULPReadableTextSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
Super::Initialize(Collection);
TickHandle = FTSTicker::GetCoreTicker().AddTicker(FTickerDelegate::CreateUObject(this, &ULPReadableTextSubsystem::Tick), 0.f);
}
void ULPReadableTextSubsystem::Deinitialize()
{
FTSTicker::GetCoreTicker().RemoveTicker(TickHandle);
Done.Empty();
Super::Deinitialize();
}
bool ULPReadableTextSubsystem::Tick(float DeltaTime)
{
SinceLastPass += DeltaTime;
if (SinceLastPass >= LPReadableText::PassInterval)
{
SinceLastPass = 0.f;
ApplyReadableStyles();
}
return true;
}
bool ULPReadableTextSubsystem::IsGameWidget(const UWidget* Widget) const
{
if (!Widget || Widget->HasAnyFlags(RF_ClassDefaultObject | RF_ArchetypeObject))
{
return false;
}
const UUserWidget* Owner = Widget->GetTypedOuter<UUserWidget>();
return Owner && !Owner->HasAnyFlags(RF_ClassDefaultObject | RF_ArchetypeObject) && Owner->GetGameInstance() == GetGameInstance();
}
void ULPReadableTextSubsystem::ApplyReadableStyles()
{
// Les widgets detruits sortent de l'ensemble.
for (auto It = Done.CreateIterator(); It; ++It)
{
if (!It->IsValid())
{
It.RemoveCurrent();
}
}
int32 NewSpins = 0;
int32 NewTextBoxes = 0;
TOptional<FSpinBoxStyle> SpinStyle;
for (TObjectIterator<USpinBox> It; It; ++It)
{
USpinBox* Spin = *It;
if (!IsGameWidget(Spin) || Done.Contains(Spin))
{
continue;
}
Done.Add(Spin);
if (LPReadableText::IsUnderSettings(Spin))
{
continue;
}
if (!SpinStyle.IsSet())
{
SpinStyle = LPReadableText::MakeSpinStyle();
}
LPReadableText::MakeSpinReadable(Spin, SpinStyle.GetValue());
LPReadableText::ApplySpinStep(Spin, LPReadableText::MenuNameOf(Spin));
++NewSpins;
const FString Name = LPReadableText::DescribeWidget(Spin);
if (!Logged.Contains(Name))
{
Logged.Add(Name);
UE_LOG(LogLifePlay, Log, TEXT("LISIB : SpinBox lisible %s"), *Name);
}
}
for (TObjectIterator<UEditableTextBox> It; It; ++It)
{
UEditableTextBox* Box = *It;
if (!IsGameWidget(Box) || Done.Contains(Box))
{
continue;
}
Done.Add(Box);
if (LPReadableText::IsUnderSettings(Box) || !LPReadableText::MakeTextBoxReadable(Box))
{
continue;
}
++NewTextBoxes;
const FString Name = LPReadableText::DescribeWidget(Box);
if (!Logged.Contains(Name))
{
Logged.Add(Name);
UE_LOG(LogLifePlay, Log, TEXT("LISIB : Text Box lisible %s"), *Name);
}
}
if (NewSpins > 0 || NewTextBoxes > 0)
{
UE_LOG(LogLifePlay, Verbose, TEXT("LISIB : passe, %d SpinBox et %d Text Box restylees"), NewSpins, NewTextBoxes);
}
}