// 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("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(); } } return false; } // Nom du menu proprietaire, sans le suffixe _C (ex. "CharGen"). FString MenuNameOf(const UWidget* Widget) { const UUserWidget* Owner = Widget->GetTypedOuter(); 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(); 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 SpinStyle; for (TObjectIterator 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 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); } }