// Fill out your copyright notice in the Description page of Project Settings. #include "SettingsDisplayWidget.h" #include "Kismet/KismetSystemLibrary.h" #include "SettingRowWidget.h" #include "SettingsMenuWidget.h" #include "SurvivalUserSettings.h" #define LOCTEXT_NAMESPACE "Reglages" namespace SettingsDisplayTab { // Ordre des modes dans la ligne. Il doit correspondre a MakeWindowMode et a // FindDisplayModeIndex : trois listes a garder alignees, d'ou leur voisinage. constexpr int32 ModeFullscreen = 0; constexpr int32 ModeBorderless = 1; constexpr int32 ModeWindowed = 2; EWindowMode::Type MakeWindowMode(int32 Index) { switch (Index) { case ModeBorderless: return EWindowMode::WindowedFullscreen; case ModeWindowed: return EWindowMode::Windowed; default: return EWindowMode::Fullscreen; } } int32 FindDisplayModeIndex(EWindowMode::Type Mode) { switch (Mode) { case EWindowMode::WindowedFullscreen: return ModeBorderless; case EWindowMode::Windowed: return ModeWindowed; default: return ModeFullscreen; } } /** * Limites de FPS proposees. 0 = illimitee, c'est la convention de * SetFrameRateLimit et non une valeur sentinelle a nous. */ const TArray& FrameRateLimits() { static const TArray Limits = { 30.f, 60.f, 90.f, 120.f, 144.f, 165.f, 240.f, 0.f }; return Limits; } int32 FindFrameRateIndex(float Limit) { const TArray& Limits = FrameRateLimits(); // Comparaison a la tolerance pres : la valeur relue du .ini a pu passer // par une conversion et ne plus etre strictement egale. for (int32 Index = 0; Index < Limits.Num(); ++Index) { if (FMath::IsNearlyEqual(Limits[Index], Limit, 0.5f)) { return Index; } } // Valeur exotique posee a la main dans le .ini : on retombe sur // "Illimitee" plutot que d'afficher n'importe quoi. return Limits.Num() - 1; } } USettingsDisplayWidget::USettingsDisplayWidget(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) { // Dans le constructeur, pour la meme raison que ConflictMessage : ecrit dans // NativeConstruct, ce texte ecraserait a chaque ouverture celui regle dans le // Blueprint. VideoConfirmMessage = LOCTEXT("VideoConfirm", "Garder ce mode d'affichage ?\nRetour automatique dans {0} s."); } void USettingsDisplayWidget::NativeConstruct() { Super::NativeConstruct(); using namespace SurvivalSettingsRange; if (DisplayModeRow) { DisplayModeRow->SetupAsChoice(LOCTEXT("ModeAffichage", "Mode d'affichage"), { LOCTEXT("PleinEcran", "Plein écran"), LOCTEXT("SansBordure", "Sans bordure"), LOCTEXT("Fenetre", "Fenêtré") }, 0); DisplayModeRow->OnIndexChanged.AddDynamic(this, &USettingsDisplayWidget::HandleDisplayModeChanged); } if (ResolutionRow) { ResolutionRow->OnIndexChanged.AddDynamic(this, &USettingsDisplayWidget::HandleResolutionChanged); } if (FrameRateLimitRow) { TArray Labels; Labels.Reserve(SettingsDisplayTab::FrameRateLimits().Num()); for (const float Limit : SettingsDisplayTab::FrameRateLimits()) { Labels.Add(FMath::IsNearlyZero(Limit) ? LOCTEXT("Illimitee", "Illimitée") : FText::AsNumber(FMath::RoundToInt(Limit))); } FrameRateLimitRow->SetupAsChoice(LOCTEXT("LimiteFPS", "Limite de FPS"), Labels, 0); FrameRateLimitRow->OnIndexChanged.AddDynamic(this, &USettingsDisplayWidget::HandleFrameRateLimitChanged); } if (VSyncRow) { VSyncRow->SetupAsChoice(LOCTEXT("VSync", "Synchronisation verticale"), { LOCTEXT("Non", "Non"), LOCTEXT("Oui", "Oui") }, 0); VSyncRow->OnIndexChanged.AddDynamic(this, &USettingsDisplayWidget::HandleVSyncChanged); } if (ResolutionScaleRow) { ResolutionScaleRow->SetupAsNumber(LOCTEXT("EchelleResolution", "Échelle de résolution"), ResolutionScaleMin, ResolutionScaleMax, ResolutionScaleStep, ResolutionScaleArrowStep, ResolutionScaleMax, LOCTEXT("Pourcent", " %")); ResolutionScaleRow->OnIndexChanged.AddDynamic(this, &USettingsDisplayWidget::HandleResolutionScaleChanged); } if (BrightnessRow) { BrightnessRow->SetupAsNumber(LOCTEXT("Luminosite", "Luminosité"), BrightnessMin, BrightnessMax, BrightnessStep, BrightnessArrowStep, BrightnessMin, LOCTEXT("Pourcent", " %")); BrightnessRow->OnIndexChanged.AddDynamic(this, &USettingsDisplayWidget::HandleBrightnessChanged); } RebuildResolutionList(); RefreshFromSettings(); } void USettingsDisplayWidget::RebuildResolutionList() { AvailableResolutions.Reset(); // La liste depend du mode : en fenetre on veut des tailles qui tiennent dans // l'ecran, pas les modes exclusifs de la carte graphique. const USurvivalUserSettings* Settings = USurvivalUserSettings::Get(); const bool bWindowed = Settings && Settings->GetFullscreenMode() == EWindowMode::Windowed; if (bWindowed) { UKismetSystemLibrary::GetConvenientWindowedResolutions(AvailableResolutions); } else { UKismetSystemLibrary::GetSupportedFullscreenResolutions(AvailableResolutions); } if (AvailableResolutions.Num() == 0) { // Ecran non interroge (editeur, machine sans mode expose) : on propose au // moins la resolution courante pour ne pas afficher une ligne vide. UE_LOG(LogTemp, Warning, TEXT("USettingsDisplayWidget : aucune resolution retournee par le systeme, repli sur la resolution courante.")); AvailableResolutions.Add(Settings ? Settings->GetScreenResolution() : FIntPoint(1920, 1080)); } FilterResolutions(Settings ? Settings->GetScreenResolution() : FIntPoint(1920, 1080)); if (!ResolutionRow) { return; } TArray Labels; Labels.Reserve(AvailableResolutions.Num()); for (const FIntPoint& Resolution : AvailableResolutions) { // FString::Printf et non FText::AsNumber : ce dernier formate selon la // locale et insererait un separateur de milliers -- "1 920 x 1 080". // Une resolution s'ecrit "1920x1080", sans espace ni separateur : c'est // une reference technique, pas une quantite. Labels.Add(FText::FromString(FString::Printf(TEXT("%dx%d"), Resolution.X, Resolution.Y))); } ResolutionRow->SetupAsChoice(LOCTEXT("Resolution", "Résolution"), Labels, FindCurrentResolutionIndex(Settings)); } void USettingsDisplayWidget::FilterResolutions(const FIntPoint& CurrentResolution) { // Des PLAGES et non un rapport exact : les resolutions commerciales ne // tombent pas juste. En 16:9, 1366x768 vaut 1.779 ; en "21:9", 2560x1080 // vaut 2.370, 3440x1440 vaut 2.389 et 3840x1600 vaut 2.400 -- trois valeurs // differentes pour un format qu'on annonce pourtant comme un seul. constexpr float WideMin = 1.74f; constexpr float WideMax = 1.81f; constexpr float UltrawideMin = 2.30f; constexpr float UltrawideMax = 2.42f; TArray Kept; Kept.Reserve(AvailableResolutions.Num()); for (const FIntPoint& Resolution : AvailableResolutions) { if (Resolution.X < MinimumResolution.X || Resolution.Y < MinimumResolution.Y) { continue; } const float Ratio = static_cast(Resolution.X) / static_cast(Resolution.Y); const bool bIsWide = Ratio >= WideMin && Ratio <= WideMax; const bool bIsUltrawide = bAllowUltrawide && Ratio >= UltrawideMin && Ratio <= UltrawideMax; // Tout le reste degage : 4:3, 5:4, 16:10, et le 32:9 des ecrans doubles, // qui deformerait le HUD sans qu'on ait rien prevu pour lui. if (!bIsWide && !bIsUltrawide) { continue; } Kept.Add(Resolution); } // La resolution courante doit rester selectionnable meme si elle ne passe pas // le filtre : sinon un joueur en 16:10 verrait la ligne afficher autre chose // que ce qu'il utilise vraiment. if (!Kept.Contains(CurrentResolution) && CurrentResolution.X > 0 && CurrentResolution.Y > 0) { Kept.Add(CurrentResolution); } // Un ecran qui ne proposerait aucun 16:9 vaut mieux non filtre que vide. if (Kept.IsEmpty()) { UE_LOG(LogTemp, Warning, TEXT("USettingsDisplayWidget : aucune resolution ne passe le filtre, la liste complete est conservee.")); return; } // Tri croissant par surface : la plus petite en haut, la native en bas. Kept.Sort([](const FIntPoint& A, const FIntPoint& B) { return static_cast(A.X) * A.Y < static_cast(B.X) * B.Y; }); AvailableResolutions = MoveTemp(Kept); } int32 USettingsDisplayWidget::FindCurrentResolutionIndex(const USurvivalUserSettings* Settings) const { if (!Settings) { return 0; } const FIntPoint Current = Settings->GetScreenResolution(); const int32 Found = AvailableResolutions.IndexOfByKey(Current); // Resolution absente de la liste (fenetre redimensionnee a la main) : on // retombe sur la plus grande, en fin de liste. return Found != INDEX_NONE ? Found : AvailableResolutions.Num() - 1; } int32 USettingsDisplayWidget::FindLargestResolutionIndex() const { int32 BestIndex = INDEX_NONE; int64 BestArea = -1; // On compare la SURFACE et non l'index : rien ne garantit que le systeme // renvoie ses modes triés, et un 2560x1080 ultralarge ne doit pas passer // devant un 1920x1200. for (int32 Index = 0; Index < AvailableResolutions.Num(); ++Index) { const int64 Area = static_cast(AvailableResolutions[Index].X) * AvailableResolutions[Index].Y; if (Area > BestArea) { BestArea = Area; BestIndex = Index; } } return BestIndex; } void USettingsDisplayWidget::RefreshFromSettings() { USurvivalUserSettings* Settings = USurvivalUserSettings::Get(); if (!Settings) { return; } if (DisplayModeRow) { DisplayModeRow->SetIndexSilently(SettingsDisplayTab::FindDisplayModeIndex(Settings->GetFullscreenMode())); } if (ResolutionRow) { ResolutionRow->SetIndexSilently(FindCurrentResolutionIndex(Settings)); } if (FrameRateLimitRow) { FrameRateLimitRow->SetIndexSilently(SettingsDisplayTab::FindFrameRateIndex(Settings->GetFrameRateLimit())); } if (VSyncRow) { VSyncRow->SetIndexSilently(Settings->IsVSyncEnabled() ? 1 : 0); } if (ResolutionScaleRow) { // On lit la valeur BRUTE en pourcentage et non la normalisee : la plage // autorisee par le moteur ne commence pas a 50 %, une valeur normalisee // ne correspondrait donc pas a ce que la ligne affiche. float CurrentNormalized = 0.f; float CurrentValue = 100.f; float MinValue = 0.f; float MaxValue = 100.f; Settings->GetResolutionScaleInformationEx(CurrentNormalized, CurrentValue, MinValue, MaxValue); ResolutionScaleRow->SetNumericValueSilently(CurrentValue); } if (BrightnessRow) { BrightnessRow->SetNumericValueSilently(Settings->GetBrightness()); } } void USettingsDisplayWidget::ResetToDefaults() { USurvivalUserSettings* Settings = USurvivalUserSettings::Get(); if (!Settings) { return; } // On ne touche PAS a la resolution ni au mode d'affichage : les remettre // "par defaut" pourrait basculer sur un mode que l'ecran ne gere pas, et le // joueur n'a pas demande ca en cliquant Reinitialiser. Settings->SetFrameRateLimit(0.f); Settings->SetVSyncEnabled(false); Settings->SetResolutionScaleNormalized(1.f); Settings->SetSurvivalDisplayToDefaults(); Settings->ApplyNonResolutionSettings(); RefreshFromSettings(); } void USettingsDisplayWidget::HandleDisplayModeChanged(int32 NewIndex) { USurvivalUserSettings* Settings = USurvivalUserSettings::Get(); if (!Settings) { return; } Settings->SetFullscreenMode(SettingsDisplayTab::MakeWindowMode(NewIndex)); // Passer en fenetre change la liste des tailles proposables. RebuildResolutionList(); // En plein ecran ou sans bordure, on bascule d'office sur la plus grande // resolution disponible -- c'est-a-dire celle de l'ecran. // // Sans ca, un jeu lance en Standalone dans une fenetre de 1280x720 garderait // cette resolution en plein ecran : l'image serait etiree sur tout l'ecran et // visiblement floue, alors que le joueur vient de demander le contraire. En // fenetre on ne touche a rien, la taille choisie est celle qu'il veut. if (NewIndex != SettingsDisplayTab::ModeWindowed) { const int32 LargestIndex = FindLargestResolutionIndex(); if (AvailableResolutions.IsValidIndex(LargestIndex)) { Settings->SetScreenResolution(AvailableResolutions[LargestIndex]); if (ResolutionRow) { ResolutionRow->SetIndexSilently(LargestIndex); } } } ApplyVideoModeAndConfirm(); } void USettingsDisplayWidget::HandleResolutionChanged(int32 NewIndex) { USurvivalUserSettings* Settings = USurvivalUserSettings::Get(); if (!Settings || !AvailableResolutions.IsValidIndex(NewIndex)) { return; } Settings->SetScreenResolution(AvailableResolutions[NewIndex]); ApplyVideoModeAndConfirm(); } void USettingsDisplayWidget::ApplyVideoModeAndConfirm() { USurvivalUserSettings* Settings = USurvivalUserSettings::Get(); if (!Settings) { return; } // ApplyResolutionSettings et non ApplySettings : on ne veut surtout pas // repousser la qualite graphique au passage, ce serait une saccade a chaque // clic de fleche. Settings->ApplyResolutionSettings(/*bCheckForCommandLineOverrides=*/false); if (!OwningMenu.IsValid()) { // Sans ecran parent, personne ne peut poser la question. On valide, au // prix du filet de securite -- mieux que de laisser un etat en suspens. Settings->ConfirmVideoMode(); return; } FOnSettingsConfirmationResult Callback; Callback.BindDynamic(this, &USettingsDisplayWidget::HandleVideoConfirmAnswered); if (!OwningMenu->RequestConfirmation(VideoConfirmMessage, VideoConfirmSeconds, Callback)) { Settings->ConfirmVideoMode(); } } void USettingsDisplayWidget::HandleVideoConfirmAnswered(bool bConfirmed) { USurvivalUserSettings* Settings = USurvivalUserSettings::Get(); if (!Settings) { return; } if (bConfirmed) { // Ce mode devient la reference vers laquelle RevertVideoMode ramenera la // prochaine fois. Settings->ConfirmVideoMode(); } else { Settings->RevertVideoMode(); Settings->ApplyResolutionSettings(/*bCheckForCommandLineOverrides=*/false); RebuildResolutionList(); } RefreshFromSettings(); } void USettingsDisplayWidget::HandleFrameRateLimitChanged(int32 NewIndex) { USurvivalUserSettings* Settings = USurvivalUserSettings::Get(); if (!Settings || !SettingsDisplayTab::FrameRateLimits().IsValidIndex(NewIndex)) { return; } Settings->SetFrameRateLimit(SettingsDisplayTab::FrameRateLimits()[NewIndex]); Settings->ApplyNonResolutionSettings(); } void USettingsDisplayWidget::HandleVSyncChanged(int32 NewIndex) { if (USurvivalUserSettings* Settings = USurvivalUserSettings::Get()) { Settings->SetVSyncEnabled(NewIndex == 1); Settings->ApplyNonResolutionSettings(); } } void USettingsDisplayWidget::HandleResolutionScaleChanged(int32 NewIndex) { USurvivalUserSettings* Settings = USurvivalUserSettings::Get(); if (!Settings || !ResolutionScaleRow) { return; } Settings->SetResolutionScaleValueEx(ResolutionScaleRow->GetNumericValue()); Settings->ApplyNonResolutionSettings(); } void USettingsDisplayWidget::HandleBrightnessChanged(int32 NewIndex) { USurvivalUserSettings* Settings = USurvivalUserSettings::Get(); if (!Settings || !BrightnessRow) { return; } Settings->SetBrightness(BrightnessRow->GetNumericValue()); // ApplySurvivalSettings et non ApplyNonResolutionSettings : la luminosite // est a nous, elle ne passe pas par le volet video du moteur. Settings->ApplySurvivalSettings(); } #undef LOCTEXT_NAMESPACE