diff --git a/Source/LifePlay/LPColorGradeSubsystem.cpp b/Source/LifePlay/LPColorGradeSubsystem.cpp new file mode 100644 index 0000000..e832d13 --- /dev/null +++ b/Source/LifePlay/LPColorGradeSubsystem.cpp @@ -0,0 +1,378 @@ +// Voir LPColorGradeSubsystem.h. + +#include "LPColorGradeSubsystem.h" + +#include "Blueprint/UserWidget.h" +#include "Components/HorizontalBox.h" +#include "Components/HorizontalBoxSlot.h" +#include "Components/PanelWidget.h" +#include "Components/ScrollBox.h" +#include "Components/ScrollBoxSlot.h" +#include "Components/SizeBox.h" +#include "Components/Slider.h" +#include "Components/TextBlock.h" +#include "Engine/GameInstance.h" +#include "Engine/PostProcessVolume.h" +#include "Engine/World.h" +#include "LifePlay.h" +#include "Misc/ConfigCacheIni.h" +#include "Misc/Paths.h" +#include "Styling/CoreStyle.h" +#include "Styling/SlateTypes.h" + +namespace LPColorGrade +{ + FString IniPath() + { + return FPaths::Combine(FPaths::ProjectSavedDir(), TEXT("Config/Windows/LPColorGrade.ini")); + } + + FString FormatValue(float Value, int32 Decimals) + { + return FString::Printf(TEXT("%.*f"), Decimals, Value); + } + + FSlateFontInfo MenuFont() + { + FSlateFontInfo Font = FCoreStyle::GetDefaultFontStyle("Regular", 20); + Font.OutlineSettings.OutlineSize = 2; + Font.OutlineSettings.bApplyOutlineToDropShadows = true; + Font.OutlineSettings.OutlineColor = FLinearColor(0.f, 0.f, 0.f, 0.8f); + return Font; + } + + void FindLook(UWidget* Widget, UTextBlock*& OutText, USlider*& OutSlider) + { + if (!Widget || (OutText && OutSlider)) + { + return; + } + if (!OutText) + { + OutText = Cast(Widget); + } + if (!OutSlider) + { + OutSlider = Cast(Widget); + } + if (UUserWidget* User = Cast(Widget)) + { + FindLook(User->GetRootWidget(), OutText, OutSlider); + } + if (UPanelWidget* Panel = Cast(Widget)) + { + for (int32 Index = 0; Index < Panel->GetChildrenCount() && !(OutText && OutSlider); ++Index) + { + FindLook(Panel->GetChildAt(Index), OutText, OutSlider); + } + } + } +} + +void ULPColorGradeSubsystem::Initialize(FSubsystemCollectionBase& Collection) +{ + Super::Initialize(Collection); + Load(); + TickHandle = FTSTicker::GetCoreTicker().AddTicker(FTickerDelegate::CreateUObject(this, &ULPColorGradeSubsystem::Tick), 0.5f); +} + +void ULPColorGradeSubsystem::Deinitialize() +{ + FTSTicker::GetCoreTicker().RemoveTicker(TickHandle); + Super::Deinitialize(); +} + +bool ULPColorGradeSubsystem::Tick(float DeltaTime) +{ + (void)DeltaTime; + EnsureVolume(); + EnsureControls(); + return true; +} + +void ULPColorGradeSubsystem::Load() +{ + const FString Ini = LPColorGrade::IniPath(); + FConfigFile File; + File.Read(Ini); + auto Read = [&File](const TCHAR* Key, float& Out) + { + FString Text; + if (File.GetString(TEXT("LPColorGrade"), Key, Text)) + { + Out = FCString::Atof(*Text); + } + }; + Read(TEXT("Brightness"), Brightness); + Read(TEXT("Saturation"), Saturation); + Read(TEXT("Contrast"), Contrast); + Read(TEXT("WhiteBalance"), WhiteBalance); + Read(TEXT("Tint"), Tint); + Brightness = FMath::Clamp(Brightness, -2.f, 2.f); + Saturation = FMath::Clamp(Saturation, 0.f, 2.f); + Contrast = FMath::Clamp(Contrast, 0.f, 2.f); + WhiteBalance = FMath::Clamp(WhiteBalance, 1500.f, 15000.f); + Tint = FMath::Clamp(Tint, -1.f, 1.f); + UE_LOG(LogLifePlay, Display, TEXT("Couleur : relu Brightness %.2f, Saturation %.2f, Contrast %.2f, White Balance %.0f, Tint %.2f depuis %s"), Brightness, Saturation, Contrast, WhiteBalance, Tint, *Ini); +} + +void ULPColorGradeSubsystem::Save() const +{ + const FString Ini = LPColorGrade::IniPath(); + FConfigFile File; + File.Read(Ini); + auto Write = [&File](const TCHAR* Key, float Value) + { + File.SetString(TEXT("LPColorGrade"), Key, *FString::SanitizeFloat(Value)); + }; + Write(TEXT("Brightness"), Brightness); + Write(TEXT("Saturation"), Saturation); + Write(TEXT("Contrast"), Contrast); + Write(TEXT("WhiteBalance"), WhiteBalance); + Write(TEXT("Tint"), Tint); + File.Write(Ini); + UE_LOG(LogLifePlay, Display, TEXT("Couleur : Brightness %.2f, Saturation %.2f, Contrast %.2f, White Balance %.0f, Tint %.2f enregistres dans %s"), Brightness, Saturation, Contrast, WhiteBalance, Tint, *Ini); +} + +void ULPColorGradeSubsystem::ApplyGrade() +{ + APostProcessVolume* Volume = GradeVolume.Get(); + if (!Volume) + { + return; + } + FPostProcessSettings& Settings = Volume->Settings; + Settings.bOverride_AutoExposureBias = true; + Settings.AutoExposureBias = Brightness; + Settings.bOverride_ColorSaturation = true; + Settings.ColorSaturation = FVector4(Saturation, Saturation, Saturation, 1.f); + Settings.bOverride_ColorContrast = true; + Settings.ColorContrast = FVector4(Contrast, Contrast, Contrast, 1.f); + Settings.bOverride_TemperatureType = true; + Settings.TemperatureType = TEMP_WhiteBalance; + Settings.bOverride_WhiteTemp = true; + Settings.WhiteTemp = WhiteBalance; + Settings.bOverride_WhiteTint = true; + Settings.WhiteTint = Tint; +} + +void ULPColorGradeSubsystem::EnsureVolume() +{ + UWorld* World = GetWorld(); + if (!World || !World->IsGameWorld()) + { + return; + } + if (GradeVolume.IsValid() && GradeVolume->GetWorld() == World) + { + ApplyGrade(); + return; + } + FActorSpawnParameters Params; + Params.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn; + Params.ObjectFlags |= RF_Transient; + APostProcessVolume* Volume = World->SpawnActor(APostProcessVolume::StaticClass(), Params); + if (!Volume) + { + return; + } + Volume->bUnbound = true; + Volume->bEnabled = true; + Volume->BlendWeight = 1.f; + Volume->Priority = 100.f; + GradeVolume = Volume; + ApplyGrade(); + UE_LOG(LogLifePlay, Display, TEXT("Couleur : exposition, saturation, contraste et balance des blancs")); +} + +USlider* ULPColorGradeSubsystem::AddRow(UScrollBox* Scroll, const TCHAR* Name, const TCHAR* Label, float Min, float Max, float Step, float Value, FName Handler, const TCHAR* Tip, const FSlateFontInfo& Font, const FSlateColor& Color, const FSliderStyle* SliderStyle) +{ + UHorizontalBox* Row = NewObject(Scroll, Name); + UTextBlock* Caption = NewObject(Row); + Caption->SetText(FText::FromString(Label)); + Caption->SetFont(Font); + Caption->SetColorAndOpacity(Color); + USizeBox* CaptionBox = NewObject(Row); + CaptionBox->SetWidthOverride(220.f); + CaptionBox->AddChild(Caption); + USlider* Slider = NewObject(Row); + if (SliderStyle) + { + Slider->SetWidgetStyle(*SliderStyle); + } + Slider->SetMinValue(Min); + Slider->SetMaxValue(Max); + Slider->SetStepSize(Step); + Slider->SetValue(Value); + UTextBlock* Readout = NewObject(Row); + Readout->SetFont(Font); + Readout->SetColorAndOpacity(Color); + if (UHorizontalBoxSlot* Slot = Row->AddChildToHorizontalBox(CaptionBox)) + { + Slot->SetVerticalAlignment(VAlign_Center); + Slot->SetPadding(FMargin(10.f, 0.f, 20.f, 0.f)); + } + if (UHorizontalBoxSlot* Slot = Row->AddChildToHorizontalBox(Slider)) + { + Slot->SetSize(FSlateChildSize(ESlateSizeRule::Fill)); + Slot->SetVerticalAlignment(VAlign_Center); + Slot->SetPadding(FMargin(0.f, 0.f, 20.f, 0.f)); + } + if (UHorizontalBoxSlot* Slot = Row->AddChildToHorizontalBox(Readout)) + { + Slot->SetVerticalAlignment(VAlign_Center); + Slot->SetPadding(FMargin(0.f, 0.f, 10.f, 0.f)); + } + if (UScrollBoxSlot* Slot = Cast(Scroll->AddChild(Row))) + { + Slot->SetPadding(FMargin(0.f, 0.f, 0.f, 10.f)); + } + Row->SetToolTipText(FText::FromString(Tip)); + FScriptDelegate Delegate; + Delegate.BindUFunction(this, Handler); + Slider->OnValueChanged.Add(Delegate); + if (Name == FString(TEXT("LPColorBrightness"))) + { + BrightnessValue = Readout; + } + else if (Name == FString(TEXT("LPColorSaturation"))) + { + SaturationValue = Readout; + } + else if (Name == FString(TEXT("LPColorContrast"))) + { + ContrastValue = Readout; + } + else if (Name == FString(TEXT("LPColorWhiteBalance"))) + { + WhiteBalanceValue = Readout; + } + else if (Name == FString(TEXT("LPColorTint"))) + { + TintValue = Readout; + } + return Slider; +} + +void ULPColorGradeSubsystem::EnsureControls() +{ + UScrollBox* Scroll = nullptr; + for (TObjectIterator It; It; ++It) + { + UScrollBox* Candidate = *It; + if (!Candidate || Candidate->GetFName() != TEXT("SettingsScrollBox") || Candidate->HasAnyFlags(RF_ClassDefaultObject | RF_ArchetypeObject)) + { + continue; + } + const UUserWidget* Owner = Candidate->GetTypedOuter(); + if (!Owner || Owner->HasAnyFlags(RF_ClassDefaultObject) || Owner->GetGameInstance() != GetGameInstance()) + { + continue; + } + Scroll = Candidate; + break; + } + if (!Scroll) + { + return; + } + for (int32 Index = 0; Index < Scroll->GetChildrenCount(); ++Index) + { + if (UWidget* Child = Scroll->GetChildAt(Index)) + { + if (Child->GetFName() == TEXT("LPColorBrightness")) + { + return; + } + } + } + UTextBlock* SampleText = nullptr; + USlider* SampleSlider = nullptr; + for (int32 Index = 0; Index < Scroll->GetChildrenCount(); ++Index) + { + LPColorGrade::FindLook(Scroll->GetChildAt(Index), SampleText, SampleSlider); + } + const FSlateFontInfo Font = SampleText ? SampleText->GetFont() : LPColorGrade::MenuFont(); + const FSlateColor Color = SampleText ? SampleText->GetColorAndOpacity() : FSlateColor(FLinearColor(1.f, 1.f, 1.f, 0.95f)); + const FSliderStyle* SliderStyle = SampleSlider ? &SampleSlider->GetWidgetStyle() : nullptr; + AddRow(Scroll, TEXT("LPColorBrightness"), TEXT("Brightness"), -2.f, 2.f, 0.05f, Brightness, GET_FUNCTION_NAME_CHECKED(ULPColorGradeSubsystem, OnBrightnessChanged), TEXT("Exposure compensation. -1 is twice as dark, +1 is twice as bright."), Font, Color, SliderStyle); + AddRow(Scroll, TEXT("LPColorSaturation"), TEXT("Saturation"), 0.f, 2.f, 0.05f, Saturation, GET_FUNCTION_NAME_CHECKED(ULPColorGradeSubsystem, OnSaturationChanged), TEXT("Color intensity. 1 is unchanged, 0 is gray."), Font, Color, SliderStyle); + AddRow(Scroll, TEXT("LPColorContrast"), TEXT("Contrast"), 0.f, 2.f, 0.05f, Contrast, GET_FUNCTION_NAME_CHECKED(ULPColorGradeSubsystem, OnContrastChanged), TEXT("Range between light and dark. 1 is unchanged."), Font, Color, SliderStyle); + AddRow(Scroll, TEXT("LPColorWhiteBalance"), TEXT("White Balance"), 1500.f, 15000.f, 50.f, WhiteBalance, GET_FUNCTION_NAME_CHECKED(ULPColorGradeSubsystem, OnWhiteBalanceChanged), TEXT("Color temperature in kelvin. 6500 is daylight."), Font, Color, SliderStyle); + AddRow(Scroll, TEXT("LPColorTint"), TEXT("Tint"), -1.f, 1.f, 0.01f, Tint, GET_FUNCTION_NAME_CHECKED(ULPColorGradeSubsystem, OnTintChanged), TEXT("Magenta to green. 0 is unchanged."), Font, Color, SliderStyle); + if (UTextBlock* Readout = BrightnessValue.Get()) + { + Readout->SetText(FText::FromString(LPColorGrade::FormatValue(Brightness, 2))); + } + if (UTextBlock* Readout = SaturationValue.Get()) + { + Readout->SetText(FText::FromString(LPColorGrade::FormatValue(Saturation, 2))); + } + if (UTextBlock* Readout = ContrastValue.Get()) + { + Readout->SetText(FText::FromString(LPColorGrade::FormatValue(Contrast, 2))); + } + if (UTextBlock* Readout = WhiteBalanceValue.Get()) + { + Readout->SetText(FText::FromString(LPColorGrade::FormatValue(WhiteBalance, 0))); + } + if (UTextBlock* Readout = TintValue.Get()) + { + Readout->SetText(FText::FromString(LPColorGrade::FormatValue(Tint, 2))); + } +} + +void ULPColorGradeSubsystem::OnBrightnessChanged(float Value) +{ + Brightness = Value; + if (UTextBlock* Readout = BrightnessValue.Get()) + { + Readout->SetText(FText::FromString(LPColorGrade::FormatValue(Value, 2))); + } + ApplyGrade(); + Save(); +} + +void ULPColorGradeSubsystem::OnSaturationChanged(float Value) +{ + Saturation = Value; + if (UTextBlock* Readout = SaturationValue.Get()) + { + Readout->SetText(FText::FromString(LPColorGrade::FormatValue(Value, 2))); + } + ApplyGrade(); + Save(); +} + +void ULPColorGradeSubsystem::OnContrastChanged(float Value) +{ + Contrast = Value; + if (UTextBlock* Readout = ContrastValue.Get()) + { + Readout->SetText(FText::FromString(LPColorGrade::FormatValue(Value, 2))); + } + ApplyGrade(); + Save(); +} + +void ULPColorGradeSubsystem::OnWhiteBalanceChanged(float Value) +{ + WhiteBalance = Value; + if (UTextBlock* Readout = WhiteBalanceValue.Get()) + { + Readout->SetText(FText::FromString(LPColorGrade::FormatValue(Value, 0))); + } + ApplyGrade(); + Save(); +} + +void ULPColorGradeSubsystem::OnTintChanged(float Value) +{ + Tint = Value; + if (UTextBlock* Readout = TintValue.Get()) + { + Readout->SetText(FText::FromString(LPColorGrade::FormatValue(Value, 2))); + } + ApplyGrade(); + Save(); +} diff --git a/Source/LifePlay/LPColorGradeSubsystem.h b/Source/LifePlay/LPColorGradeSubsystem.h new file mode 100644 index 0000000..14dbf57 --- /dev/null +++ b/Source/LifePlay/LPColorGradeSubsystem.h @@ -0,0 +1,64 @@ +// Luminosite, saturation, contraste et balance des blancs. +// +// Ce sont les reglages globaux du panneau Color Grading d'Unreal (FPostProcessSettings) : +// la compensation d'exposition, ColorSaturation, ColorContrast, et la balance des blancs +// (WhiteTemp / WhiteTint, methode White Balance). Ils s'appliquent a toute la vue par un +// volume de post-traitement non borne. Les valeurs neutres du moteur ne changent pas l'image. +// Enregistre dans Saved/Config/Windows/LPColorGrade.ini, pas dans GameSettings.sav. + +#pragma once + +#include "CoreMinimal.h" +#include "Containers/Ticker.h" +#include "Styling/SlateTypes.h" +#include "Subsystems/GameInstanceSubsystem.h" +#include "LPColorGradeSubsystem.generated.h" + +class APostProcessVolume; +class UScrollBox; +class USlider; +class UTextBlock; + +UCLASS() +class LIFEPLAY_API ULPColorGradeSubsystem : public UGameInstanceSubsystem +{ + GENERATED_BODY() + +public: + virtual void Initialize(FSubsystemCollectionBase& Collection) override; + virtual void Deinitialize() override; + + UFUNCTION() + void OnBrightnessChanged(float Value); + UFUNCTION() + void OnSaturationChanged(float Value); + UFUNCTION() + void OnContrastChanged(float Value); + UFUNCTION() + void OnWhiteBalanceChanged(float Value); + UFUNCTION() + void OnTintChanged(float Value); + +private: + bool Tick(float DeltaTime); + void EnsureControls(); + void EnsureVolume(); + void ApplyGrade(); + void Load(); + void Save() const; + USlider* AddRow(UScrollBox* Scroll, const TCHAR* Name, const TCHAR* Label, float Min, float Max, float Step, float Value, FName Handler, const TCHAR* Tip, const FSlateFontInfo& Font, const FSlateColor& Color, const FSliderStyle* SliderStyle); + + FTSTicker::FDelegateHandle TickHandle; + TWeakObjectPtr GradeVolume; + TWeakObjectPtr BrightnessValue; + TWeakObjectPtr SaturationValue; + TWeakObjectPtr ContrastValue; + TWeakObjectPtr WhiteBalanceValue; + TWeakObjectPtr TintValue; + + float Brightness = 0.f; + float Saturation = 1.f; + float Contrast = 1.f; + float WhiteBalance = 6500.f; + float Tint = 0.f; +}; diff --git a/Source/LifePlay/LPSettingsSubsystem.cpp b/Source/LifePlay/LPSettingsSubsystem.cpp new file mode 100644 index 0000000..ffbbb10 --- /dev/null +++ b/Source/LifePlay/LPSettingsSubsystem.cpp @@ -0,0 +1,612 @@ +// 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/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/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 "UObject/TextProperty.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; + + 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; + } + + 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(); + } + Text += TEXT(" ") + Widget->GetToolTipText().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 -1; + } + if (Widget && Widget->GetName().StartsWith(TEXT("LPColor"))) + { + return TabVideo; + } + 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")) || Text.Contains(TEXT("shadow")) || Text.Contains(TEXT("quality")) + || 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"))) + { + 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; + } + + 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); + TickHandle = FTSTicker::GetCoreTicker().AddTicker(FTickerDelegate::CreateUObject(this, &ULPSettingsSubsystem::Tick), 0.5f); +} + +void ULPSettingsSubsystem::Deinitialize() +{ + FTSTicker::GetCoreTicker().RemoveTicker(TickHandle); + Super::Deinitialize(); +} + +bool ULPSettingsSubsystem::Tick(float DeltaTime) +{ + (void)DeltaTime; + PlaceSettingsButton(); + UScrollBox* Scroll = nullptr; + for (TObjectIterator It; It; ++It) + { + UScrollBox* Candidate = *It; + if (!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() != GetGameInstance()) + { + continue; + } + Scroll = Candidate; + break; + } + if (!Scroll) + { + return true; + } + SettingsList = Scroll; + const bool bOpen = LPSettingsUi::IsOpen(Scroll); + if (TabBar.IsValid()) + { + TabBar->SetVisibility(bOpen ? ESlateVisibility::Visible : ESlateVisibility::Collapsed); + } + if (KeyboardHost.IsValid() && !bOpen) + { + KeyboardHost->SetVisibility(ESlateVisibility::Collapsed); + bKeyboardPage = false; + } + if (!bOpen) + { + return true; + } + EnsureTabs(Scroll); + if (Scroll->GetChildrenCount() != LastChildCount) + { + LastChildCount = Scroll->GetChildrenCount(); + 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; + } + const UUserWidget* MenuOwner = Box->GetTypedOuter(); + if (!MenuOwner || MenuOwner->HasAnyFlags(RF_ClassDefaultObject) || MenuOwner->GetGameInstance() != GetGameInstance()) + { + 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; + ApplyRows(); + UE_LOG(LogLifePlay, Display, TEXT("Reglages : onglets General, Audio, Video, Keyboard, Lewd, Misc")); +} + +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); + const bool bShow = Category < 0 || (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); } diff --git a/Source/LifePlay/LPSettingsSubsystem.h b/Source/LifePlay/LPSettingsSubsystem.h new file mode 100644 index 0000000..77a33cc --- /dev/null +++ b/Source/LifePlay/LPSettingsSubsystem.h @@ -0,0 +1,67 @@ +// Reglages par onglets, et entree Settings de l'ecran titre (entre Mod Manager et Donate). +// +// Les lignes existent deja dans MainMenu.SettingsScrollBox (cases et curseurs du Blueprint). +// Ce sous-systeme ne les recree pas : il les montre ou les cache selon l'onglet. +// General, Audio, Video, Keyboard (la liste des touches), Lewd, puis Misc. +// Aucun Blueprint n'est modifie. + +#pragma once + +#include "CoreMinimal.h" +#include "Containers/Ticker.h" +#include "Subsystems/GameInstanceSubsystem.h" +#include "LPSettingsSubsystem.generated.h" + +class UButton; +class UCanvasPanel; +class UHorizontalBox; +class UNativeWidgetHost; +class UScrollBox; +class UTextBlock; +class UVerticalBox; +class UWidget; + +UCLASS() +class LIFEPLAY_API ULPSettingsSubsystem : public UGameInstanceSubsystem +{ + GENERATED_BODY() + +public: + virtual void Initialize(FSubsystemCollectionBase& Collection) override; + virtual void Deinitialize() override; + + UFUNCTION() + void ShowAudio(); + UFUNCTION() + void ShowVideo(); + UFUNCTION() + void ShowLewd(); + UFUNCTION() + void ShowGeneral(); + UFUNCTION() + void ShowKeyboard(); + UFUNCTION() + void ShowMisc(); + + UFUNCTION() + void OpenSettings(); + +private: + bool Tick(float DeltaTime); + void PlaceSettingsButton(); + void EnsureTabs(UScrollBox* Scroll); + void SelectTab(int32 Index); + void ApplyRows(); + + FTSTicker::FDelegateHandle TickHandle; + int32 ActiveTab = 0; + int32 LastChildCount = -1; + bool bKeyboardPage = false; + + TWeakObjectPtr SourceSettings; + TWeakObjectPtr OrderedMenu; + TWeakObjectPtr SettingsList; + TWeakObjectPtr TabBar; + TWeakObjectPtr KeyboardHost; + TArray> TabLabels; +};