// Reglages par onglets. Voir LPSettingsSubsystem.h. #include "LPSettingsSubsystem.h" #include "Blueprint/UserWidget.h" #include "Components/Button.h" #include "Components/ButtonSlot.h" #include "Components/CanvasPanel.h" #include "Components/CanvasPanelSlot.h" #include "Components/CheckBox.h" #include "Components/HorizontalBox.h" #include "Components/HorizontalBoxSlot.h" #include "Components/Image.h" #include "Components/NativeWidgetHost.h" #include "Components/Overlay.h" #include "Components/OverlaySlot.h" #include "Components/ScrollBox.h" #include "Components/SizeBox.h" #include "Components/Slider.h" #include "Components/SpinBox.h" #include "Components/TextBlock.h" #include "Components/PanelWidget.h" #include "Components/VerticalBox.h" #include "Components/VerticalBoxSlot.h" #include "Engine/GameInstance.h" #include "Engine/Texture2D.h" #include "LPKeyboardSubsystem.h" #include "LifePlay.h" #include "Styling/CoreStyle.h" #include "Styling/SlateTypes.h" #include "UObject/TextProperty.h" #include "Widgets/SNullWidget.h" namespace LPSettingsUi { const TCHAR* TabNames[] = { TEXT("General"), TEXT("Audio"), TEXT("Video"), TEXT("Keyboard"), TEXT("Lewd"), TEXT("Misc") }; constexpr int32 TabGeneral = 0; constexpr int32 TabAudio = 1; constexpr int32 TabVideo = 2; constexpr int32 TabKeyboard = 3; constexpr int32 TabLewd = 4; constexpr int32 TabMisc = 5; constexpr int32 TabAlways = -1; constexpr int32 TabHidden = -2; FString VolumeLabel(const FString& Text) { if (Text.Equals(TEXT("Volume - Sex Moans"), ESearchCase::IgnoreCase) || Text.Equals(TEXT("Volume - Sex Moan"), ESearchCase::IgnoreCase)) { return TEXT("Volume - SFX"); } if (Text.Equals(TEXT("Volume - Sex"), ESearchCase::IgnoreCase)) { return TEXT("Volume - Dialogue"); } return Text; } void RenameVolumes(UWidget* Widget) { if (!Widget) { return; } if (FTextProperty* Setting = CastField(Widget->GetClass()->FindPropertyByName(TEXT("SettingText")))) { const FString Next = VolumeLabel(Setting->GetPropertyValue_InContainer(Widget).ToString()); if (Next != Setting->GetPropertyValue_InContainer(Widget).ToString()) { Setting->SetPropertyValue_InContainer(Widget, FText::FromString(Next)); } } if (UTextBlock* Block = Cast(Widget)) { const FString Next = VolumeLabel(Block->GetText().ToString()); if (Next != Block->GetText().ToString()) { Block->SetText(FText::FromString(Next)); } } if (UUserWidget* User = Cast(Widget)) { RenameVolumes(User->GetRootWidget()); } if (UPanelWidget* Panel = Cast(Widget)) { for (int32 Index = 0; Index < Panel->GetChildrenCount(); ++Index) { RenameVolumes(Panel->GetChildAt(Index)); } } } bool IsOpen(const UWidget* Widget) { if (!Widget) { return false; } const ESlateVisibility Visibility = Widget->GetVisibility(); return Visibility != ESlateVisibility::Collapsed && Visibility != ESlateVisibility::Hidden; } // Lot SET1-V1. GI.CreateMainMenuWidget cree un MainMenu neuf a chaque ouverture (CreateWidget + AddToViewport) et // GI.ClearMainMenuWidget le retire seulement du viewport (RemoveFromParent) : les anciennes instances gardent leur // SettingsScrollBox en memoire jusqu'au ramasse-miettes. Seule l'instance a l'ecran compte. bool IsUsableObject(const UObject* Object) { return IsValid(Object) && !Object->HasAnyFlags(RF_BeginDestroyed | RF_FinishDestroyed); } // Menu vivant, ajoute au viewport (UWidget::IsInViewport : AddToViewport ou AddToPlayerScreen) et visible. bool IsMenuOnScreen(const UUserWidget* Menu) { return IsUsableObject(Menu) && Menu->IsInViewport() && IsOpen(Menu); } // Liste des reglages de l'instance de MainMenu qui est a l'ecran, nulle s'il n'y en a aucune. A egalite entre deux // instances a l'ecran : la liste ouverte d'abord, puis celle deja suivie (Current). UScrollBox* FindOnScreenSettingsList(const UGameInstance* GI, const UScrollBox* Current) { UScrollBox* Best = nullptr; int32 BestScore = -1; for (TObjectIterator It; It; ++It) { UScrollBox* Candidate = *It; if (!IsUsableObject(Candidate) || Candidate->GetFName() != TEXT("SettingsScrollBox") || Candidate->HasAnyFlags(RF_ClassDefaultObject | RF_ArchetypeObject)) { continue; } const UUserWidget* MenuOwner = Candidate->GetTypedOuter(); if (!MenuOwner || MenuOwner->HasAnyFlags(RF_ClassDefaultObject) || MenuOwner->GetGameInstance() != GI || !IsMenuOnScreen(MenuOwner)) { continue; } const int32 Score = (IsOpen(Candidate) ? 2 : 0) + (Candidate == Current ? 1 : 0); if (Score > BestScore) { Best = Candidate; BestScore = Score; } } return Best; } // Ecran titre (MainMenu.TitleScreen, variable du Blueprint) ou partie. Repli : Mod Manager de l'ecran titre visible. bool IsTitleMenu(const UUserWidget* Menu) { if (!Menu) { return false; } if (const FBoolProperty* Title = CastField(Menu->GetClass()->FindPropertyByName(TEXT("TitleScreen")))) { return Title->GetPropertyValue_InContainer(Menu); } const UWidget* TitleMods = Menu->GetWidgetFromName(TEXT("ModManagerTitleButton")); return TitleMods && IsOpen(TitleMods); } // Retire la barre d'onglets et l'hote du clavier d'une instance quittee. La page Slate du clavier est unique // (ULPKeyboardSubsystem::TakeEmbeddedPage rend la meme tant qu'elle vit) : l'ancien hote la lache avant que le // nouveau la prenne. Une capture de touche en cours est annulee (la page change de place). void DropTabs(UGameInstance* GI, TWeakObjectPtr& Bar, TWeakObjectPtr& Host, TArray>& Labels) { UHorizontalBox* OldBar = Bar.Get(); UNativeWidgetHost* OldHost = Host.Get(); if (OldBar || OldHost) { if (ULPKeyboardSubsystem* Keys = GI ? GI->GetSubsystem() : nullptr) { if (Keys->IsWaiting()) { Keys->CancelWait(); } } const UUserWidget* OldMenu = OldBar ? OldBar->GetTypedOuter() : OldHost->GetTypedOuter(); UE_LOG(LogLifePlay, Display, TEXT("Reglages : onglets retires de %s (instance quittee)"), OldMenu ? *OldMenu->GetName() : TEXT("?")); } if (OldHost) { OldHost->SetContent(SNullWidget::NullWidget); if (IsUsableObject(OldHost->GetParent())) { OldHost->RemoveFromParent(); } } if (OldBar && IsUsableObject(OldBar->GetParent())) { OldBar->RemoveFromParent(); } Bar.Reset(); Host.Reset(); Labels.Reset(); } // Libelle visible seulement : les infobulles provoquent des faux positifs // (ex. "on screen" → Video, "prefer quality" → Video pour NewAnimationsOnly). FString RowText(const UWidget* Widget) { FString Text; if (!Widget) { return Text; } if (const FTextProperty* Setting = CastField(Widget->GetClass()->FindPropertyByName(TEXT("SettingText")))) { Text += Setting->GetPropertyValue_InContainer(Widget).ToString(); } TArray Pending; Pending.Add(const_cast(Widget)); while (Pending.Num() > 0) { UWidget* Current = Pending.Pop(EAllowShrinking::No); if (const UTextBlock* Block = Cast(Current)) { Text += TEXT(" ") + Block->GetText().ToString(); } if (const UUserWidget* User = Cast(Current)) { if (UWidget* Root = User->GetRootWidget()) { Pending.Add(Root); } } if (const UPanelWidget* Panel = Cast(Current)) { for (int32 Index = 0; Index < Panel->GetChildrenCount(); ++Index) { Pending.Add(Panel->GetChildAt(Index)); } } } return Text; } int32 CategoryOf(const UWidget* Widget) { if (Widget && Widget->GetFName() == TEXT("BackButton")) { return TabAlways; } // Reglages qualite video remplaces par les lignes LPVideo* (LPVideoQualitySubsystem). if (Widget && (Widget->GetFName() == TEXT("ShadowQuality") || Widget->GetFName() == TEXT("PostProcessQuality") || Widget->GetFName() == TEXT("PostProcessAAQuality"))) { return TabHidden; } // Noms stables : evite les collisions de mots-cles sur le libelle. if (Widget) { const FName Name = Widget->GetFName(); if (Name == TEXT("NewAnimationsOnly")) { return TabGeneral; } if (Name == TEXT("ShowFPS") || Name == TEXT("ScreenPercentage") || Name == TEXT("FullscreenButton") || Name == TEXT("ToonShading") || Name == TEXT("Censore") || Name == TEXT("CPUSkinning") || Name == TEXT("DisableFurEffect")) { return TabVideo; } } if (Widget && (Widget->GetName().StartsWith(TEXT("LPColor")) || Widget->GetName().StartsWith(TEXT("LPVideo")))) { return TabVideo; } // Lot U7 : cases des controles de mods (ULPModInfoSubsystem). if (Widget && Widget->GetName().StartsWith(TEXT("LPModCheck"))) { return TabMisc; } const FString Text = RowText(Widget).ToLower(); if (Text.Contains(TEXT("volume")) || Text.Contains(TEXT("audio")) || Text.Contains(TEXT("sound")) || Text.Contains(TEXT("music"))) { return TabAudio; } if (Text.Contains(TEXT("fps")) || Text.Contains(TEXT("screen percentage")) || Text.Contains(TEXT("shadow")) || Text.Contains(TEXT("fullscreen")) || Text.Contains(TEXT("window")) || Text.Contains(TEXT("toon")) || Text.Contains(TEXT("depth of field")) || Text.Contains(TEXT("censor")) || Text.Contains(TEXT("morph")) || Text.Contains(TEXT("dog")) || Text.Contains(TEXT("jiggle")) || Text.Contains(TEXT("resolution")) || Text.Contains(TEXT("vsync")) || Text.Contains(TEXT("brightness")) || (Text.Contains(TEXT("quality")) && (Text.Contains(TEXT("shadow")) || Text.Contains(TEXT("aa ")) || Text.Contains(TEXT("anti-alias")) || Text.Contains(TEXT("post process")) || Text.Contains(TEXT("pp ")) || Text.Contains(TEXT("overall")) || Text.Contains(TEXT("texture")) || Text.Contains(TEXT("effect"))))) { return TabVideo; } if (Text.Contains(TEXT("sex")) || Text.Contains(TEXT("condom")) || Text.Contains(TEXT("squirt")) || Text.Contains(TEXT("pregnan")) || Text.Contains(TEXT("underwear")) || Text.Contains(TEXT("orientation")) || Text.Contains(TEXT("trans")) || Text.Contains(TEXT("relationship")) || Text.Contains(TEXT("moan")) || Text.Contains(TEXT("lewd"))) { return TabLewd; } if (Text.Contains(TEXT("difficult")) || Text.Contains(TEXT("clock")) || Text.Contains(TEXT("camera")) || Text.Contains(TEXT("scene")) || Text.Contains(TEXT("pause")) || Text.Contains(TEXT("timeout")) || Text.Contains(TEXT("tutorial")) || Text.Contains(TEXT("marker")) || Text.Contains(TEXT("building")) || Text.Contains(TEXT("animation")) || Text.Contains(TEXT("action filter")) || Text.Contains(TEXT("dye")) || Text.Contains(TEXT("stat")) || Text.Contains(TEXT("child")) || Text.Contains(TEXT("redress"))) { return TabGeneral; } return TabMisc; } void WireMainMenuGI(UUserWidget* Menu, UGameInstance* GI) { if (!Menu || !GI) { return; } FObjectPropertyBase* Prop = CastField(Menu->GetClass()->FindPropertyByName(TEXT("GI"))); if (!Prop || Prop->GetObjectPropertyValue_InContainer(Menu)) { return; } if (!GI->IsA(Prop->PropertyClass)) { return; } Prop->SetObjectPropertyValue_InContainer(Menu, GI); UE_LOG(LogLifePlay, Display, TEXT("Reglages : MainMenu.GI rattache (evite les spinbox / cases None sur l'ecran titre)")); } void CollectCheckBoxes(UWidget* Widget, TArray& Out) { if (!Widget) { return; } if (UCheckBox* Box = Cast(Widget)) { Out.Add(Box); return; } if (UUserWidget* User = Cast(Widget)) { CollectCheckBoxes(User->GetRootWidget(), Out); } if (UPanelWidget* Panel = Cast(Widget)) { for (int32 Index = 0; Index < Panel->GetChildrenCount(); ++Index) { CollectCheckBoxes(Panel->GetChildAt(Index), Out); } } } void CollectSpinBoxes(UWidget* Widget, TArray& Out) { if (!Widget) { return; } if (USpinBox* Box = Cast(Widget)) { Out.Add(Box); return; } if (UUserWidget* User = Cast(Widget)) { CollectSpinBoxes(User->GetRootWidget(), Out); } if (UPanelWidget* Panel = Cast(Widget)) { for (int32 Index = 0; Index < Panel->GetChildrenCount(); ++Index) { CollectSpinBoxes(Panel->GetChildAt(Index), Out); } } } void CollectSliders(UWidget* Widget, TArray& Out) { if (!Widget) { return; } if (USlider* Slider = Cast(Widget)) { Out.Add(Slider); return; } if (UUserWidget* User = Cast(Widget)) { CollectSliders(User->GetRootWidget(), Out); } if (UPanelWidget* Panel = Cast(Widget)) { for (int32 Index = 0; Index < Panel->GetChildrenCount(); ++Index) { CollectSliders(Panel->GetChildAt(Index), Out); } } } void TintBrush(FSlateBrush& Brush, const FLinearColor& Tint) { Brush.TintColor = FSlateColor(Tint); } // Cases et spinboxes BP : style Slate clair → illisible sur fond flou clair. // Le curseur Contrast du color grading les revelait sans corriger le style. void HardenSettingsControls(UScrollBox* Scroll) { if (!Scroll) { return; } FCheckBoxStyle CheckStyle = FCoreStyle::Get().GetWidgetStyle("Checkbox"); const FLinearColor Unchecked(0.08f, 0.08f, 0.08f, 1.f); const FLinearColor UncheckedHover(0.18f, 0.18f, 0.18f, 1.f); const FLinearColor Checked(0.12f, 0.42f, 0.95f, 1.f); const FLinearColor CheckedHover(0.2f, 0.5f, 1.f, 1.f); TintBrush(CheckStyle.UncheckedImage, Unchecked); TintBrush(CheckStyle.UncheckedHoveredImage, UncheckedHover); TintBrush(CheckStyle.UncheckedPressedImage, Unchecked); TintBrush(CheckStyle.CheckedImage, Checked); TintBrush(CheckStyle.CheckedHoveredImage, CheckedHover); TintBrush(CheckStyle.CheckedPressedImage, Checked); TintBrush(CheckStyle.UndeterminedImage, Unchecked); TintBrush(CheckStyle.UndeterminedHoveredImage, UncheckedHover); TintBrush(CheckStyle.UndeterminedPressedImage, Unchecked); CheckStyle.BorderBackgroundColor = FSlateColor(FLinearColor(1.f, 1.f, 1.f, 1.f)); CheckStyle.ForegroundColor = FSlateColor(FLinearColor(1.f, 1.f, 1.f, 1.f)); FSpinBoxStyle SpinStyle = FCoreStyle::Get().GetWidgetStyle("SpinBox"); const FLinearColor SpinBg(0.06f, 0.06f, 0.06f, 0.95f); const FLinearColor SpinBgHover(0.12f, 0.12f, 0.12f, 0.98f); const FLinearColor SpinBgActive(0.16f, 0.16f, 0.16f, 1.f); TintBrush(SpinStyle.BackgroundBrush, SpinBg); TintBrush(SpinStyle.HoveredBackgroundBrush, SpinBgHover); TintBrush(SpinStyle.ActiveBackgroundBrush, SpinBgActive); TintBrush(SpinStyle.ActiveFillBrush, FLinearColor(0.2f, 0.45f, 0.9f, 0.85f)); SpinStyle.ForegroundColor = FSlateColor(FLinearColor(1.f, 1.f, 1.f, 1.f)); // Lot STC : slider de vitesse de camera seulement (les sliders LPColor* et LPVideo* gardent leur style). Barre sombre comme le fond des spinboxes, // poignee bleue comme leur remplissage. Style et couleurs seulement : aucune valeur ecrite (pas de SetValue). const FLinearColor SliderBar(0.06f, 0.06f, 0.06f, 0.95f); const FLinearColor SliderBarHover(0.12f, 0.12f, 0.12f, 0.98f); const FLinearColor SliderHandle(0.2f, 0.45f, 0.9f, 1.f); const FLinearColor SliderHandleHover(0.3f, 0.55f, 1.f, 1.f); TArray Boxes; TArray Spins; TArray Sliders; for (int32 Index = 0; Index < Scroll->GetChildrenCount(); ++Index) { CollectCheckBoxes(Scroll->GetChildAt(Index), Boxes); CollectSpinBoxes(Scroll->GetChildAt(Index), Spins); CollectSliders(Scroll->GetChildAt(Index), Sliders); } for (UCheckBox* Box : Boxes) { if (!Box) { continue; } Box->SetIsEnabled(true); Box->SetRenderOpacity(1.f); Box->SetWidgetStyle(CheckStyle); { FWidgetTransform Xform = Box->GetRenderTransform(); if (FMath::Max(FMath::Abs(Xform.Scale.X), FMath::Abs(Xform.Scale.Y)) < 1.5f) { Xform.Scale = FVector2D(2.f, 2.f); Box->SetRenderTransform(Xform); } } } for (USpinBox* Spin : Spins) { if (!Spin) { continue; } Spin->SetIsEnabled(true); Spin->SetRenderOpacity(1.f); 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 < 18.f) { Font.Size = 18.f; } Spin->SetFont(Font); } for (USlider* Slider : Sliders) { if (!Slider || Slider->GetFName() != TEXT("SliderCameraSpeed")) { continue; } // Style propre du slider (pinceaux et epaisseur gardes), seules les teintes changent. FSliderStyle SliderStyle = Slider->GetWidgetStyle(); TintBrush(SliderStyle.NormalBarImage, SliderBar); TintBrush(SliderStyle.HoveredBarImage, SliderBarHover); TintBrush(SliderStyle.NormalThumbImage, SliderHandle); TintBrush(SliderStyle.HoveredThumbImage, SliderHandleHover); Slider->SetWidgetStyle(SliderStyle); // SSlider::OnPaint multiplie la teinte du pinceau par ces couleurs : blanc = teintes ci-dessus telles quelles. Slider->SetSliderBarColor(FLinearColor::White); Slider->SetSliderHandleColor(FLinearColor::White); } if (UWidget* ShowFPS = [&]() -> UWidget* { for (int32 Index = 0; Index < Scroll->GetChildrenCount(); ++Index) { if (UWidget* Child = Scroll->GetChildAt(Index)) { if (Child->GetFName() == TEXT("ShowFPS")) { return Child; } } } return nullptr; }()) { ShowFPS->SetIsEnabled(true); ShowFPS->SetRenderOpacity(1.f); } } UTextBlock* FirstText(UWidget* Widget) { if (!Widget) { return nullptr; } if (UTextBlock* Text = Cast(Widget)) { return Text; } if (UUserWidget* User = Cast(Widget)) { if (UTextBlock* Text = FirstText(User->GetRootWidget())) { return Text; } } if (UPanelWidget* Panel = Cast(Widget)) { for (int32 Index = 0; Index < Panel->GetChildrenCount(); ++Index) { if (UTextBlock* Text = FirstText(Panel->GetChildAt(Index))) { return Text; } } } return nullptr; } UImage* RoundIcon(UWidget* Owner, const TCHAR* TexturePath, float Size) { UImage* Image = NewObject(Owner); if (UTexture2D* Texture = LoadObject(nullptr, TexturePath)) { FSlateBrush Brush; Brush.SetResourceObject(Texture); Brush.ImageSize = FVector2D(Size, Size); Image->SetBrush(Brush); } return Image; } } void ULPSettingsSubsystem::Initialize(FSubsystemCollectionBase& Collection) { Super::Initialize(Collection); // Chaque frame : le BP Settings peut reforcer Visible sur toutes les lignes entre deux ticks. TickHandle = FTSTicker::GetCoreTicker().AddTicker(FTickerDelegate::CreateUObject(this, &ULPSettingsSubsystem::Tick), 0.f); } void ULPSettingsSubsystem::Deinitialize() { FTSTicker::GetCoreTicker().RemoveTicker(TickHandle); Super::Deinitialize(); } UScrollBox* ULPSettingsSubsystem::FindOnScreenSettingsList(const UGameInstance* GI, const UScrollBox* Current) { // SET1-V1b : meme choix que le Tick des onglets (LPSettingsUi::FindOnScreenSettingsList), expose aux autres sous-systemes. return LPSettingsUi::FindOnScreenSettingsList(GI, Current); } bool ULPSettingsSubsystem::Tick(float DeltaTime) { (void)DeltaTime; PlaceSettingsButton(); // SET1-V1 : la liste de l'instance de MainMenu qui est a l'ecran, pas la premiere trouvee (une ancienne instance, // retiree du viewport et pas encore collectee, vient souvent avant). Aucune a l'ecran : Scroll reste nul et les // reglages sont vus fermes (capture de touche annulee, page du clavier lachee) ; les onglets restent sur leur // ancienne instance jusqu'au prochain rattachement. UScrollBox* Scroll = LPSettingsUi::FindOnScreenSettingsList(GetGameInstance(), SettingsList.Get()); if (Scroll) { // Autre instance, ou barre qui n'est plus sur le Canvas de cette liste : on retire les onglets de l'ancienne // (plus de TabBar valide sur elle), EnsureTabs les repose sur celle-ci. Etat par instance remis a zero. const bool bOtherList = Scroll != SettingsList.Get(); const bool bBarElsewhere = TabBar.IsValid() && TabBar->GetParent() != Scroll->GetParent(); if (bOtherList || bBarElsewhere) { LPSettingsUi::DropTabs(GetGameInstance(), TabBar, KeyboardHost, TabLabels); bKeyboardPage = false; bControlsHardened = false; LastChildCount = -1; } SettingsList = Scroll; } const bool bOpen = Scroll && LPSettingsUi::IsOpen(Scroll); if (TabBar.IsValid()) { TabBar->SetVisibility(bOpen ? ESlateVisibility::Visible : ESlateVisibility::Collapsed); } if (KeyboardHost.IsValid() && !bOpen) { KeyboardHost->SetVisibility(ESlateVisibility::Collapsed); bKeyboardPage = false; // KEY2-DIAG-01 : reglages fermes pendant une capture de touche : la capture est annulee, sinon la page // integree compterait encore comme menu ouvert (marche, camera et relevage coupes). if (ULPKeyboardSubsystem* Keys = GetGameInstance() ? GetGameInstance()->GetSubsystem() : nullptr) { if (Keys->IsWaiting()) { Keys->CancelWait(); } } } if (!bOpen) { bControlsHardened = false; return true; } if (UUserWidget* Menu = Scroll->GetTypedOuter()) { LPSettingsUi::WireMainMenuGI(Menu, GetGameInstance()); } EnsureTabs(Scroll); const int32 ChildCount = Scroll->GetChildrenCount(); if (!bControlsHardened || ChildCount != LastChildCount) { LPSettingsUi::HardenSettingsControls(Scroll); bControlsHardened = true; LastChildCount = ChildCount; } ApplyRows(); return true; } void ULPSettingsSubsystem::OpenSettings() { if (UButton* Source = SourceSettings.Get()) { Source->OnClicked.Broadcast(); } } void ULPSettingsSubsystem::PlaceSettingsButton() { UVerticalBox* Menu = nullptr; UButton* TitleMods = nullptr; UButton* Original = nullptr; UButton* Ours = nullptr; for (TObjectIterator It; It; ++It) { UVerticalBox* Box = *It; if (!Box || Box->GetFName() != TEXT("MainMenuOptions") || Box->HasAnyFlags(RF_ClassDefaultObject | RF_ArchetypeObject)) { continue; } // SET1-V1 : seulement le menu a l'ecran. Une ancienne instance (retiree du viewport, pas encore collectee) // ferait poser le bouton et SourceSettings sur un widget que le joueur ne voit pas. const UUserWidget* MenuOwner = Box->GetTypedOuter(); if (!MenuOwner || MenuOwner->HasAnyFlags(RF_ClassDefaultObject) || MenuOwner->GetGameInstance() != GetGameInstance() || !LPSettingsUi::IsMenuOnScreen(MenuOwner)) { continue; } UButton* FoundTitle = nullptr; for (int32 Index = 0; Index < Box->GetChildrenCount(); ++Index) { if (UButton* Child = Cast(Box->GetChildAt(Index))) { if (Child->GetFName() == TEXT("ModManagerTitleButton")) { FoundTitle = Child; break; } } } if (FoundTitle) { Menu = Box; TitleMods = FoundTitle; break; } } if (!Menu || !TitleMods) { return; } for (int32 Index = 0; Index < Menu->GetChildrenCount(); ++Index) { UButton* Child = Cast(Menu->GetChildAt(Index)); if (!Child) { continue; } if (Child->GetFName() == TEXT("SettingsButton")) { Original = Child; } else if (Child->GetFName() == TEXT("LPSettingsTitleButton")) { Ours = Child; } } if (Original) { SourceSettings = Original; } if (!Ours) { Ours = NewObject(Menu, TEXT("LPSettingsTitleButton")); Ours->SetStyle(TitleMods->GetStyle()); UOverlay* Icon = NewObject(Ours); UImage* Circle = LPSettingsUi::RoundIcon(Icon, TEXT("/Game/Heavy/New/whitecircle.whitecircle"), 50.f); UImage* Gear = LPSettingsUi::RoundIcon(Icon, TEXT("/Game/Heavy/New/settings.settings"), 42.f); if (UOverlaySlot* Slot = Cast(Icon->AddChild(Circle))) { Slot->SetHorizontalAlignment(HAlign_Fill); Slot->SetVerticalAlignment(VAlign_Fill); } if (UOverlaySlot* Slot = Cast(Icon->AddChild(Gear))) { Slot->SetHorizontalAlignment(HAlign_Fill); Slot->SetVerticalAlignment(VAlign_Fill); Slot->SetPadding(FMargin(6.f)); } USizeBox* IconFrame = NewObject(Ours); IconFrame->SetWidthOverride(50.f); IconFrame->SetHeightOverride(50.f); IconFrame->AddChild(Icon); UTextBlock* Label = NewObject(Ours); Label->SetText(FText::FromString(TEXT("Settings"))); if (const UTextBlock* Sample = LPSettingsUi::FirstText(TitleMods)) { Label->SetFont(Sample->GetFont()); Label->SetColorAndOpacity(Sample->GetColorAndOpacity()); Label->SetShadowOffset(Sample->GetShadowOffset()); } else { Label->SetFont(FCoreStyle::GetDefaultFontStyle("Regular", 26)); } UHorizontalBox* Row = NewObject(Ours); if (UHorizontalBoxSlot* Slot = Row->AddChildToHorizontalBox(IconFrame)) { Slot->SetPadding(FMargin(0.f, 0.f, 10.f, 0.f)); Slot->SetVerticalAlignment(VAlign_Center); } if (UHorizontalBoxSlot* Slot = Row->AddChildToHorizontalBox(Label)) { Slot->SetVerticalAlignment(VAlign_Center); } Ours->SetContent(Row); if (UButtonSlot* ContentSlot = Cast(Row->Slot)) { ContentSlot->SetHorizontalAlignment(HAlign_Left); ContentSlot->SetVerticalAlignment(VAlign_Center); } if (Original) { Ours->SetToolTipText(Original->GetToolTipText()); } FScriptDelegate Delegate; Delegate.BindUFunction(this, GET_FUNCTION_NAME_CHECKED(ULPSettingsSubsystem, OpenSettings)); Ours->OnClicked.Add(Delegate); UE_LOG(LogLifePlay, Display, TEXT("Reglages : entree Settings ajoutee sur l'ecran titre")); } // InsertChildAt ne réordonne que le tableau UMG. La boîte Slate, elle, garde le bouton en dernier, // donc sous Exit. On reconstruit les emplacements pour que l'ordre peint soit celui du tableau. if (OrderedMenu.Get() != Menu) { struct FKept { UWidget* Widget = nullptr; FMargin Padding; EHorizontalAlignment HAlign = HAlign_Fill; EVerticalAlignment VAlign = VAlign_Fill; FSlateChildSize Size; }; TArray Kept; for (int32 Index = 0; Index < Menu->GetChildrenCount(); ++Index) { UWidget* Child = Menu->GetChildAt(Index); if (!Child || Child == Ours) { continue; } FKept Item; Item.Widget = Child; if (const UVerticalBoxSlot* Slot = Cast(Child->Slot)) { Item.Padding = Slot->GetPadding(); Item.HAlign = Slot->GetHorizontalAlignment(); Item.VAlign = Slot->GetVerticalAlignment(); Item.Size = Slot->GetSize(); } Kept.Add(Item); } FKept SettingsItem; SettingsItem.Widget = Ours; SettingsItem.Padding = FMargin(0.f, 0.f, 0.f, 10.f); SettingsItem.HAlign = HAlign_Fill; if (const UVerticalBoxSlot* TitleSlot = Cast(TitleMods->Slot)) { SettingsItem.Padding = TitleSlot->GetPadding(); SettingsItem.HAlign = TitleSlot->GetHorizontalAlignment(); SettingsItem.VAlign = TitleSlot->GetVerticalAlignment(); SettingsItem.Size = TitleSlot->GetSize(); } int32 InsertAt = Kept.Num(); for (int32 Index = 0; Index < Kept.Num(); ++Index) { if (Kept[Index].Widget == TitleMods) { InsertAt = Index + 1; break; } } Kept.Insert(SettingsItem, InsertAt); Menu->ClearChildren(); for (const FKept& Item : Kept) { if (UVerticalBoxSlot* Slot = Cast(Menu->AddChild(Item.Widget))) { Slot->SetPadding(Item.Padding); Slot->SetHorizontalAlignment(Item.HAlign); Slot->SetVerticalAlignment(Item.VAlign); Slot->SetSize(Item.Size); } } OrderedMenu = Menu; UE_LOG(LogLifePlay, Display, TEXT("Reglages : Settings place apres Mod Manager")); } const ESlateVisibility TitleVisibility = TitleMods->GetVisibility(); if (Ours->GetVisibility() != TitleVisibility) { Ours->SetVisibility(TitleVisibility); } } void ULPSettingsSubsystem::EnsureTabs(UScrollBox* Scroll) { if (TabBar.IsValid()) { return; } UCanvasPanel* Canvas = Cast(Scroll->GetParent()); UCanvasPanelSlot* ScrollSlot = Cast(Scroll->Slot); if (!Canvas || !ScrollSlot) { return; } UHorizontalBox* Bar = NewObject(Canvas, TEXT("LPSettingsTabs")); UCanvasPanelSlot* BarSlot = Canvas->AddChildToCanvas(Bar); if (!BarSlot) { return; } BarSlot->SetAnchors(ScrollSlot->GetAnchors()); BarSlot->SetAlignment(ScrollSlot->GetAlignment()); BarSlot->SetPosition(ScrollSlot->GetPosition() - FVector2D(0.f, 46.f)); BarSlot->SetSize(FVector2D(ScrollSlot->GetSize().X, 40.f)); BarSlot->SetZOrder(ScrollSlot->GetZOrder() + 1); TabBar = Bar; TabLabels.Reset(); const FName Clicks[] = { GET_FUNCTION_NAME_CHECKED(ULPSettingsSubsystem, ShowGeneral), GET_FUNCTION_NAME_CHECKED(ULPSettingsSubsystem, ShowAudio), GET_FUNCTION_NAME_CHECKED(ULPSettingsSubsystem, ShowVideo), GET_FUNCTION_NAME_CHECKED(ULPSettingsSubsystem, ShowKeyboard), GET_FUNCTION_NAME_CHECKED(ULPSettingsSubsystem, ShowLewd), GET_FUNCTION_NAME_CHECKED(ULPSettingsSubsystem, ShowMisc) }; for (int32 Index = 0; Index < UE_ARRAY_COUNT(LPSettingsUi::TabNames); ++Index) { UButton* Button = NewObject(Bar); UTextBlock* Label = NewObject(Button); Label->SetText(FText::FromString(LPSettingsUi::TabNames[Index])); Label->SetFont(FCoreStyle::GetDefaultFontStyle("Bold", 14)); Button->SetContent(Label); FButtonStyle Style = Button->GetStyle(); Style.NormalPadding = FMargin(10.f, 4.f); Style.PressedPadding = FMargin(10.f, 5.f, 10.f, 3.f); Button->SetStyle(Style); if (UHorizontalBoxSlot* Slot = Cast(Bar->AddChild(Button))) { Slot->SetPadding(FMargin(3.f, 0.f)); Slot->SetSize(FSlateChildSize(ESlateSizeRule::Automatic)); Slot->SetVerticalAlignment(VAlign_Center); } FScriptDelegate Delegate; Delegate.BindUFunction(this, Clicks[Index]); Button->OnClicked.Add(Delegate); TabLabels.Add(Label); } UNativeWidgetHost* Host = NewObject(Canvas, TEXT("LPSettingsKeyboard")); if (UCanvasPanelSlot* HostSlot = Canvas->AddChildToCanvas(Host)) { HostSlot->SetAnchors(ScrollSlot->GetAnchors()); HostSlot->SetAlignment(ScrollSlot->GetAlignment()); HostSlot->SetPosition(ScrollSlot->GetPosition() + FVector2D(0.f, 48.f)); const FVector2D Size = ScrollSlot->GetSize(); HostSlot->SetSize(FVector2D(Size.X, FMath::Max(120.f, Size.Y - 48.f))); HostSlot->SetZOrder(ScrollSlot->GetZOrder() + 1); } Host->SetVisibility(ESlateVisibility::Collapsed); KeyboardHost = Host; ActiveTab = 0; bKeyboardPage = false; ApplyRows(); UE_LOG(LogLifePlay, Display, TEXT("Reglages : onglets General, Audio, Video, Keyboard, Lewd, Misc")); const UUserWidget* Menu = Scroll->GetTypedOuter(); UE_LOG(LogLifePlay, Display, TEXT("Reglages : onglets poses sur %s (%s)"), Menu ? *Menu->GetName() : TEXT("?"), LPSettingsUi::IsTitleMenu(Menu) ? TEXT("ecran titre") : TEXT("partie")); } void ULPSettingsSubsystem::SelectTab(int32 Index) { ActiveTab = Index; ApplyRows(); } void ULPSettingsSubsystem::ApplyRows() { UScrollBox* Scroll = SettingsList.Get(); if (!Scroll) { return; } for (int32 Index = 0; Index < TabLabels.Num(); ++Index) { if (UTextBlock* Label = TabLabels[Index].Get()) { const bool bOn = Index == ActiveTab; Label->SetColorAndOpacity(FSlateColor(bOn ? FLinearColor(1.f, 0.92f, 0.45f) : FLinearColor(0.75f, 0.75f, 0.75f))); } } for (int32 Index = 0; Index < Scroll->GetChildrenCount(); ++Index) { UWidget* Child = Scroll->GetChildAt(Index); if (!Child || Child->GetFName() == TEXT("LPKeyboardRow")) { if (Child) { Child->SetVisibility(ESlateVisibility::Collapsed); } continue; } LPSettingsUi::RenameVolumes(Child); const int32 Category = LPSettingsUi::CategoryOf(Child); // -1 : toujours visible (BackButton) ; -2 : toujours replie. const bool bShow = Category == LPSettingsUi::TabAlways || (Category != LPSettingsUi::TabHidden && ActiveTab != LPSettingsUi::TabKeyboard && Category == ActiveTab); Child->SetVisibility(bShow ? ESlateVisibility::Visible : ESlateVisibility::Collapsed); } const bool bKeyboard = ActiveTab == LPSettingsUi::TabKeyboard; if (UNativeWidgetHost* Host = KeyboardHost.Get()) { Host->SetVisibility(bKeyboard ? ESlateVisibility::Visible : ESlateVisibility::Collapsed); if (bKeyboard && !bKeyboardPage) { if (ULPKeyboardSubsystem* Keys = GetGameInstance()->GetSubsystem()) { Host->SetContent(Keys->TakeEmbeddedPage()); bKeyboardPage = true; } } } } void ULPSettingsSubsystem::ShowGeneral() { SelectTab(LPSettingsUi::TabGeneral); } void ULPSettingsSubsystem::ShowAudio() { SelectTab(LPSettingsUi::TabAudio); } void ULPSettingsSubsystem::ShowVideo() { SelectTab(LPSettingsUi::TabVideo); } void ULPSettingsSubsystem::ShowKeyboard() { SelectTab(LPSettingsUi::TabKeyboard); } void ULPSettingsSubsystem::ShowLewd() { SelectTab(LPSettingsUi::TabLewd); } void ULPSettingsSubsystem::ShowMisc() { SelectTab(LPSettingsUi::TabMisc); }