// Copyright Epic Games, Inc. All Rights Reserved. #pragma once #include "CoreMinimal.h" #include "Framework/MultiBox/MultiBoxBuilder.h" #include "Misc/Attribute.h" #include "Layout/Margin.h" #include "Layout/Visibility.h" #include "Misc/NotifyHook.h" #include "Widgets/DeclarativeSyntaxSupport.h" #include "Widgets/SWidget.h" #include "Widgets/SCompoundWidget.h" #include "Widgets/SBoxPanel.h" #include "Widgets/Input/SComboButton.h" #include "Styling/SlateTypes.h" #include "Misc/Paths.h" #include "Styling/CoreStyle.h" #include "HAL/FileManager.h" #include "Styling/AppStyle.h" #include "IDetailCustomization.h" #include "IPropertyUtilities.h" #include "PropertyHandle.h" #include "DetailLayoutBuilder.h" #include "DetailCategoryBuilder.h" #include "Widgets/Text/STextBlock.h" #include "DetailWidgetRow.h" #include "Internationalization/Culture.h" #include "Widgets/Images/SImage.h" #include "Widgets/Input/SCheckBox.h" #include "Settings/ProjectPackagingSettings.h" #include "Settings/EditorExperimentalSettings.h" #include "PropertyRestriction.h" #include "Widgets/Views/SMultipleOptionTable.h" #include "DesktopPlatformModule.h" #include "ILauncherServicesModule.h" #define LOCTEXT_NAMESPACE "FProjectPackagingSettingsCustomization" class SCulturePickerRowWidget : public SCompoundWidget { SLATE_BEGIN_ARGS(SCulturePickerRowWidget){} SLATE_END_ARGS() public: void Construct(const FArguments& InArgs, FCulturePtr InCulture, TAttribute InIsFilteringCultures) { Culture = InCulture; IsFilteringCultures = InIsFilteringCultures; // Identify if this culture has localization data. { const TArray LocalizedCultureNames = FTextLocalizationManager::Get().GetLocalizedCultureNames(ELocalizationLoadFlags::Game); const TArray LocalizedCultures = FInternationalization::Get().GetAvailableCultures(LocalizedCultureNames, true); HasLocalizationData = LocalizedCultures.Contains(Culture.ToSharedRef()); } ChildSlot [ SNew(SHorizontalBox) +SHorizontalBox::Slot() .AutoWidth() .Padding(FMargin(3.0, 2.0)) .VAlign(VAlign_Center) [ // Warning Icon for whether or not this culture has localization data. SNew(SImage) .Image( FCoreStyle::Get().GetBrush("Icons.Warning") ) .Visibility(this, &SCulturePickerRowWidget::HandleWarningImageVisibility) .ToolTipText(LOCTEXT("NotLocalizedWarning", "This project does not have localization data (translations) for this culture.")) ] +SHorizontalBox::Slot() .FillWidth(1.0f) .VAlign(VAlign_Center) [ // Display name of culture. SNew(STextBlock) .Text(FText::FromString(Culture->GetDisplayName())) .ToolTipText(FText::FromString(Culture->GetName())) ] ]; } EVisibility HandleWarningImageVisibility() const { // Don't show the warning image if this culture has localization data. // Collapse the widget entirely if we are filtering to only show cultures that have it - gets rid of awkward empty column of space. bool bIsFilteringCultures = IsFilteringCultures.IsBound() ? IsFilteringCultures.Get() : false; return bIsFilteringCultures ? EVisibility::Collapsed : (HasLocalizationData ? EVisibility::Hidden : EVisibility::Visible); } private: FCulturePtr Culture; TAttribute IsFilteringCultures; bool HasLocalizationData; }; /** * Implements a details view customization for UProjectPackagingSettingsCustomization objects. */ class FProjectPackagingSettingsCustomization : public IDetailCustomization { public: // IDetailCustomization interface virtual void CustomizeDetails( IDetailLayoutBuilder& LayoutBuilder ) override { CustomizeProjectCategory(LayoutBuilder); CustomizePackagingCategory(LayoutBuilder); CustomizeCustomBuildsCategory(LayoutBuilder); } public: /** * Creates a new instance. * * @return A new struct customization for play-in settings. */ static TSharedRef MakeInstance( ) { return MakeShareable(new FProjectPackagingSettingsCustomization()); } protected: enum class EFilterCulturesChoices { /** * Only show cultures that have localization data. */ OnlyLocalizedCultures, /** * Show all available cultures. */ AllAvailableCultures }; FProjectPackagingSettingsCustomization() : FilterCulturesChoice(EFilterCulturesChoices::AllAvailableCultures) , IsInBatchSelectOperation(false) { } /** * Customizes the Project property category. * * @param LayoutBuilder The layout builder. */ void CustomizeProjectCategory( IDetailLayoutBuilder& LayoutBuilder ) { TArray PackagingConfigurations = UProjectPackagingSettings::GetValidPackageConfigurations(); TSharedRef BuildConfigurationRestriction = MakeShareable(new FPropertyRestriction(LOCTEXT("ConfigurationRestrictionReason", "This configuration is not valid for this project. DebugGame configurations are not available in Content-Only or Launcher projects, and client/server configurations require the appropriate targets.."))); const UEnum* const ProjectPackagingBuildConfigurationsEnum = StaticEnum(); for (int Idx = 0; Idx < (int)EProjectPackagingBuildConfigurations::PPBC_MAX; Idx++) { EProjectPackagingBuildConfigurations Configuration = (EProjectPackagingBuildConfigurations)Idx; if (!PackagingConfigurations.Contains(Configuration)) { BuildConfigurationRestriction->AddDisabledValue(ProjectPackagingBuildConfigurationsEnum->GetNameStringByValue(Idx)); } } TSharedRef BuildConfigurationHandle = LayoutBuilder.GetProperty("BuildConfiguration"); BuildConfigurationHandle->AddRestriction(BuildConfigurationRestriction); } /** * Customizes the Packaging property category. * * @param LayoutBuilder The layout builder. */ void CustomizePackagingCategory( IDetailLayoutBuilder& LayoutBuilder ) { IDetailCategoryBuilder& PackagingCategory = LayoutBuilder.EditCategory("Packaging"); { CulturesPropertyHandle = LayoutBuilder.GetProperty("CulturesToStage", UProjectPackagingSettings::StaticClass()); CulturesPropertyHandle->MarkHiddenByCustomization(); CulturesPropertyArrayHandle = CulturesPropertyHandle->AsArray(); PopulateCultureList(); PackagingCategory.AddCustomRow(LOCTEXT("CulturesToStageLabel", "Languages To Package"), true) .NameContent() .HAlign(HAlign_Fill) .VAlign(VAlign_Top) [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .AutoWidth() [ CulturesPropertyHandle->CreatePropertyNameWidget() ] + SHorizontalBox::Slot() .AutoWidth() [ SNew(SImage) .Image(FAppStyle::GetBrush(TEXT("Icons.Error"))) .ToolTipText(LOCTEXT("NoCulturesToStageSelectedError", "At least one language must be selected or fatal errors may occur when launching games.")) .Visibility(this, &FProjectPackagingSettingsCustomization::HandleNoCulturesErrorIconVisibility) ] ] .ValueContent() .HAlign(HAlign_Fill) .VAlign(VAlign_Fill) [ SNew(SVerticalBox) + SVerticalBox::Slot() .AutoHeight() .Padding(0.0f, 4.0f) .VAlign(VAlign_Center) [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .AutoWidth() [ // all cultures radio button SNew(SCheckBox) .IsChecked(this, &FProjectPackagingSettingsCustomization::HandleShowCulturesCheckBoxIsChecked, EFilterCulturesChoices::AllAvailableCultures) .OnCheckStateChanged(this, &FProjectPackagingSettingsCustomization::HandleShowCulturesCheckBoxCheckStateChanged, EFilterCulturesChoices::AllAvailableCultures) .Style(FAppStyle::Get(), "RadioButton") [ SNew(STextBlock) .Text(LOCTEXT("AllCulturesCheckBoxText", "Show All")) ] ] + SHorizontalBox::Slot() .FillWidth(1.0f) .Padding(8.0f, 0.0f, 0.0f, 0.0f) [ // localized cultures radio button SNew(SCheckBox) .IsChecked(this, &FProjectPackagingSettingsCustomization::HandleShowCulturesCheckBoxIsChecked, EFilterCulturesChoices::OnlyLocalizedCultures) .OnCheckStateChanged(this, &FProjectPackagingSettingsCustomization::HandleShowCulturesCheckBoxCheckStateChanged, EFilterCulturesChoices::OnlyLocalizedCultures) .Style(FAppStyle::Get(), "RadioButton") [ SNew(STextBlock) .Text(LOCTEXT("CookedCulturesCheckBoxText", "Show Localized")) ] ] ] + SVerticalBox::Slot() .AutoHeight() [ SAssignNew(Table, SMultipleOptionTable, &CultureList) .OnPreBatchSelect(this, &FProjectPackagingSettingsCustomization::OnPreBatchSelect) .OnPostBatchSelect(this, &FProjectPackagingSettingsCustomization::OnPostBatchSelect) .OnGenerateOptionWidget(this, &FProjectPackagingSettingsCustomization::GenerateWidgetForCulture) .OnOptionSelectionChanged(this, &FProjectPackagingSettingsCustomization::OnCultureSelectionChanged) .IsOptionSelected(this, &FProjectPackagingSettingsCustomization::IsCultureSelected) .ListHeight(100.0f) ] ]; } } void PopulateCultureList() { switch(FilterCulturesChoice) { case EFilterCulturesChoices::AllAvailableCultures: { TArray CultureNames; FInternationalization::Get().GetCultureNames(CultureNames); CultureList.Reset(); for(const FString& CultureName : CultureNames) { CultureList.Add(FInternationalization::Get().GetCulture(CultureName)); } } break; case EFilterCulturesChoices::OnlyLocalizedCultures: { const TArray LocalizedCultureNames = FTextLocalizationManager::Get().GetLocalizedCultureNames(ELocalizationLoadFlags::Game); const TArray LocalizedCultureList = FInternationalization::Get().GetAvailableCultures(LocalizedCultureNames, true); CultureList.Reset(); CultureList.Append(LocalizedCultureList); } break; default: checkf(false, TEXT("Unknown EFilterCulturesChoices")); break; } } EVisibility HandleNoCulturesErrorIconVisibility() const { TArray RawData; CulturesPropertyHandle->AccessRawData(RawData); TArray* RawCultureStringArray = reinterpret_cast*>(RawData[0]); return RawCultureStringArray->Num() ? EVisibility::Hidden : EVisibility::Visible; } ECheckBoxState HandleShowCulturesCheckBoxIsChecked( EFilterCulturesChoices Choice ) const { if (FilterCulturesChoice == Choice) { return ECheckBoxState::Checked; } return ECheckBoxState::Unchecked; } void HandleShowCulturesCheckBoxCheckStateChanged( ECheckBoxState NewState, EFilterCulturesChoices Choice ) { if (NewState == ECheckBoxState::Checked) { FilterCulturesChoice = Choice; } PopulateCultureList(); Table->RequestTableRefresh(); } void AddCulture(FString CultureName) { if(!IsInBatchSelectOperation) { CulturesPropertyHandle->NotifyPreChange(); } TArray RawData; CulturesPropertyHandle->AccessRawData(RawData); TArray* RawCultureStringArray = reinterpret_cast*>(RawData[0]); RawCultureStringArray->Add(CultureName); if(!IsInBatchSelectOperation) { CulturesPropertyHandle->NotifyPostChange(EPropertyChangeType::ArrayAdd); } } void RemoveCulture(FString CultureName) { if(!IsInBatchSelectOperation) { CulturesPropertyHandle->NotifyPreChange(); } TArray RawData; CulturesPropertyHandle->AccessRawData(RawData); TArray* RawCultureStringArray = reinterpret_cast*>(RawData[0]); RawCultureStringArray->Remove(CultureName); if(!IsInBatchSelectOperation) { CulturesPropertyHandle->NotifyPostChange(EPropertyChangeType::ArrayRemove); } } bool IsFilteringCultures() const { return FilterCulturesChoice == EFilterCulturesChoices::OnlyLocalizedCultures; } void OnPreBatchSelect() { IsInBatchSelectOperation = true; CulturesPropertyHandle->NotifyPreChange(); } void OnPostBatchSelect() { CulturesPropertyHandle->NotifyPostChange(EPropertyChangeType::ValueSet); IsInBatchSelectOperation = false; } TSharedRef GenerateWidgetForCulture(FCulturePtr Culture) { return SNew(SCulturePickerRowWidget, Culture, TAttribute(this, &FProjectPackagingSettingsCustomization::IsFilteringCultures)); } void OnCultureSelectionChanged(bool IsSelected, FCulturePtr Culture) { if(IsSelected) { AddCulture(Culture->GetName()); } else { RemoveCulture(Culture->GetName()); } } bool IsCultureSelected(FCulturePtr Culture) { FString CultureName = Culture->GetName(); uint32 ElementCount; CulturesPropertyArrayHandle->GetNumElements(ElementCount); for(uint32 Index = 0; Index < ElementCount; ++Index) { const TSharedRef PropertyHandle = CulturesPropertyArrayHandle->GetElement(Index); FString CultureNameAtIndex; PropertyHandle->GetValue(CultureNameAtIndex); if(CultureNameAtIndex == CultureName) { return true; } } return false; } /** * Customizes the Custom Builds property category. * * @param LayoutBuilder The layout builder. */ void CustomizeCustomBuildsCategory( IDetailLayoutBuilder& LayoutBuilder ) { TSharedPtr PropertyUtilities = LayoutBuilder.GetPropertyUtilities(); ILauncherServicesModule& LauncherServicesModule = FModuleManager::LoadModuleChecked("LauncherServices"); ILauncherProfileManagerRef LauncherProfileManager = LauncherServicesModule.GetProfileManager(); IDetailCategoryBuilder& CustomBuildsCategory = LayoutBuilder.EditCategory("CustomBuilds", FText::GetEmpty(), ECategoryPriority::Important); { CustomBuildsCategory.AddCustomRow(FText::GetEmpty(), false) .WholeRowWidget [ SNew(SBorder) .BorderImage(FAppStyle::GetBrush("ToolPanel.GroupBorder")) [ SNew(SHorizontalBox) // combo button to import from project launcher. hidden if there are no custom laucher profiles +SHorizontalBox::Slot() .AutoWidth() .VAlign(VAlign_Center) .Padding(8.4) [ SNew(SComboButton) .ComboButtonStyle(FAppStyle::Get(), "SimpleComboButton") .OnGetMenuContent_Lambda( [this, PropertyUtilities] { return CreateImportFromProjectLauncherMenu(PropertyUtilities); } ) .Visibility_Lambda( [LauncherProfileManager] { return LauncherProfileManager->GetAllProfiles().IsEmpty() ? EVisibility::Collapsed : EVisibility::Visible; }) .ButtonContent() [ SNew(SHorizontalBox) +SHorizontalBox::Slot() .AutoWidth() .VAlign(VAlign_Center) .HAlign(HAlign_Center) [ SNew(SImage) .Image(FAppStyle::GetBrush("Launcher.TabIcon")) ] +SHorizontalBox::Slot() .AutoWidth() .VAlign(VAlign_Center) .Padding(4,0,0,0) [ SNew(STextBlock) .Text(LOCTEXT("ImportProjectLauncher", "Import From Project Launcher")) ] ] ] ] ] .Visibility(TAttribute::Create(TAttribute::FGetter::CreateLambda([]() { // only visible if the user has enabled this in the experimental settings return GetDefault()->bProjectCustomBuildTools ? EVisibility::Visible : EVisibility::Collapsed; }))) ; } } TSharedRef CreateImportFromProjectLauncherMenu( TSharedPtr PropertyUtilities ) { const bool bCloseAfterSelection = true; FMenuBuilder MenuBuilder(bCloseAfterSelection, nullptr, nullptr, true); ILauncherServicesModule& LauncherServicesModule = FModuleManager::LoadModuleChecked("LauncherServices"); for (const ILauncherProfilePtr& LauncherProfilePtr : LauncherServicesModule.GetProfileManager()->GetAllProfiles()) { MenuBuilder.AddMenuEntry( FText::FromString(LauncherProfilePtr->GetName()), FText::FromString(LauncherProfilePtr->GetDescription()), FSlateIcon(), FUIAction ( FExecuteAction::CreateRaw( this, &FProjectPackagingSettingsCustomization::ImportFromLauncherProfile, LauncherProfilePtr, PropertyUtilities ) ), NAME_None, EUserInterfaceActionType::Button); } return MenuBuilder.MakeWidget(); } void ImportFromLauncherProfile( const ILauncherProfilePtr LauncherProfilePtr, TSharedPtr PropertyUtilities ) { ILauncherServicesModule& LauncherServicesModule = FModuleManager::LoadModuleChecked("LauncherServices"); // grab the project packaging settings UProjectPackagingSettings* ProjectPackagingSettings = UProjectPackagingSettings::StaticClass()->GetDefaultObject(); if (ProjectPackagingSettings == nullptr) { return; } // ensure the name is unique (Turnkey builds a dictionary using the Name) FString ProfileName = LauncherProfilePtr->GetName(); int UniqueId = 1; while (ProjectPackagingSettings->ProjectCustomBuilds.ContainsByPredicate( [ProfileName](const FProjectBuildSettings& Other) { return ProfileName == Other.Name; } ) ) { ProfileName = FString::Printf( TEXT("%s %d"), *LauncherProfilePtr->GetName(), UniqueId++ ); } // add a new item FProjectBuildSettings& ProjectBuildSettings = ProjectPackagingSettings->ProjectCustomBuilds.AddDefaulted_GetRef(); ProjectBuildSettings.Name = ProfileName; ProjectBuildSettings.HelpText = LauncherProfilePtr->GetDescription().IsEmpty() ? LauncherProfilePtr->GetName() : LauncherProfilePtr->GetDescription(); ProjectBuildSettings.SpecificPlatforms = LauncherProfilePtr->GetCookedPlatforms(); if (ProjectBuildSettings.SpecificPlatforms.Num() == 0 && !LauncherProfilePtr->GetDefaultDeployPlatform().IsNone()) { ProjectBuildSettings.SpecificPlatforms.Add(LauncherProfilePtr->GetDefaultDeployPlatform().ToString()); } ProjectBuildSettings.BuildCookRunParams = LauncherServicesModule.GetProfileManager()->MakeBuildCookRunParamsForProjectCustomBuild(LauncherProfilePtr.ToSharedRef(), ProjectBuildSettings.SpecificPlatforms); // signal that the property has changed FNotifyHook* NotifyHook = PropertyUtilities->GetNotifyHook(); FProperty* Property = UProjectPackagingSettings::StaticClass()->FindPropertyByName("ProjectCustomBuilds"); if (NotifyHook != nullptr && Property != nullptr) { TArray NotifyTopLevelObjects; NotifyTopLevelObjects.Add(ProjectPackagingSettings); FEditPropertyChain PropertyChain; PropertyChain.AddHead(Property); FPropertyChangedEvent ChangeEvent(Property, EPropertyChangeType::ValueSet, MakeArrayView(NotifyTopLevelObjects)); NotifyHook->NotifyPostChange(ChangeEvent, &PropertyChain); } } private: TArray CultureList; TSharedPtr CulturesPropertyHandle; TSharedPtr CulturesPropertyArrayHandle; EFilterCulturesChoices FilterCulturesChoice; TSharedPtr< SMultipleOptionTable > Table; bool IsInBatchSelectOperation; }; #undef LOCTEXT_NAMESPACE