// Copyright Epic Games, Inc. All Rights Reserved. #include "LocalizedAssetTools.h" #include "AssetDefinitionRegistry.h" #include "AssetRegistry/IAssetRegistry.h" #include "AssetTools.h" #include "AssetToolsLog.h" #include "AssetToolsModule.h" #include "Internationalization/PackageLocalizationUtil.h" #include "ISourceControlModule.h" #include "ISourceControlProvider.h" #include "Logging/LogMacros.h" #include "Misc/App.h" #include "Misc/MessageDialog.h" #include "SFileListReportDialog.h" #include "SourceControlHelpers.h" #include "SourceControlPreferences.h" #include "UObject/UObjectGlobals.h" #include "Widgets/Input/SButton.h" #define LOCTEXT_NAMESPACE "LocalizedAssetTools" class SIncludeLocalizedVariantsDialog : public SFileListReportDialog { private: bool bAllowOperationCanceling = true; ELocalizedVariantsInclusion RecommendedBehavior = ELocalizedVariantsInclusion::Include; ELocalizedVariantsInclusion Result = ELocalizedVariantsInclusion::Include; public: static ELocalizedVariantsInclusion OpenIncludeListDialog(const FText& InTitle, const FText& InHeader, const TArray& InFiles, ELocalizedVariantsInclusion RecommendedBehavior = ELocalizedVariantsInclusion::Include, bool bAllowOperationCanceling = true, ELocalizedVariantsInclusion UnattendedDefaultBehavior = ELocalizedVariantsInclusion::Exclude) { // Make sure that if bAllowOperationCanceling is false then the default behavior should never be "Cancel". ensureAlways(bAllowOperationCanceling || (!bAllowOperationCanceling && RecommendedBehavior != ELocalizedVariantsInclusion::Cancel)); if (FApp::IsUnattended() || GIsRunningUnattendedScript) { return UnattendedDefaultBehavior; } TSharedRef FileListReportDialogRef = SNew(SIncludeLocalizedVariantsDialog).Header(InHeader).Files(InFiles); FileListReportDialogRef->bAllowOperationCanceling = bAllowOperationCanceling; FileListReportDialogRef->RecommendedBehavior = RecommendedBehavior; FileListReportDialogRef->Result = RecommendedBehavior; FileListReportDialogRef->bOpenAsModal = true; FileListReportDialogRef->bAllowTitleBarX = bAllowOperationCanceling; FileListReportDialogRef->Title = InTitle; CreateWindow(FileListReportDialogRef); // Modal window is closed so the result should be accessible now return FileListReportDialogRef->Result; } protected: FReply OnIncludeSelected() { Result = ELocalizedVariantsInclusion::Include; return CloseWindow(); } FReply OnExcludeSelected() { Result = ELocalizedVariantsInclusion::Exclude; return CloseWindow(); } FReply OnCancelSelected() { Result = ELocalizedVariantsInclusion::Cancel; return CloseWindow(); } virtual void OnClosedWithTitleBarX(const TSharedRef& Window) { if (bAllowOperationCanceling) { Result = ELocalizedVariantsInclusion::Cancel; } // Otherwise, Result is already defined, let's use that value } virtual TSharedRef ConstructButtons(const FArguments& InArgs) override { TSharedRef HorizontalBox = SNew(SHorizontalBox) + SHorizontalBox::Slot() .AutoWidth() .Padding(4, 0) .HAlign(HAlign_Right) [ SNew(SButton) .OnClicked(this, &SIncludeLocalizedVariantsDialog::OnIncludeSelected) .Text(LOCTEXT("IncludeLocalizedVariantsDialogIncludeButtonText", "Include")) .ButtonStyle(FAppStyle::Get(), RecommendedBehavior == ELocalizedVariantsInclusion::Include ? "FlatButton.Primary" : "FlatButton.Default") ] + SHorizontalBox::Slot() .AutoWidth() .Padding(4, 0) .HAlign(HAlign_Right) [ SNew(SButton) .OnClicked(this, &SIncludeLocalizedVariantsDialog::OnExcludeSelected) .Text(LOCTEXT("IncludeLocalizedVariantsDialogExcludeButtonText", "Exclude")) .ButtonStyle(FAppStyle::Get(), RecommendedBehavior == ELocalizedVariantsInclusion::Exclude ? "FlatButton.Primary" : "FlatButton.Default") ]; if (bAllowOperationCanceling) { HorizontalBox->AddSlot() .AutoWidth() .Padding(4, 0) .HAlign(HAlign_Right) [ SNew(SButton) .OnClicked(this, &SIncludeLocalizedVariantsDialog::OnCancelSelected) .Text(LOCTEXT("IncludeLocalizedVariantsDialogCancelButtonText", "Cancel")) .ButtonStyle(FAppStyle::Get(), RecommendedBehavior == ELocalizedVariantsInclusion::Cancel ? "FlatButton.Primary" : "FlatButton.Default") ]; } return HorizontalBox; } virtual FReply OnKeyDown(const FGeometry& MyGeometry, const FKeyEvent& InKeyEvent) override { if (InKeyEvent.GetKey() == EKeys::Enter) { switch (RecommendedBehavior) { case ELocalizedVariantsInclusion::Include: return OnIncludeSelected(); case ELocalizedVariantsInclusion::Exclude: return OnExcludeSelected(); case ELocalizedVariantsInclusion::Cancel: default: return OnCancelSelected(); } } else if(InKeyEvent.GetKey() == EKeys::Escape) { return OnCancelSelected(); } return SFileListReportDialog::OnKeyDown(MyGeometry, InKeyEvent); } }; FLocalizedAssetTools::FLocalizedAssetTools() : RevisionControlIsNotAvailableWarningText(LOCTEXT("RevisionControlIsRequiredToChangeLocalizableAssets", "Revision Control is required to move/rename/delete localizable assets for this project and it is currently not accessible.")) , FilesNeedToBeOnDiskWarningText(LOCTEXT("FilesToSyncDialogTitle", "Files in Revision Control need to be on disk")) { } bool FLocalizedAssetTools::CanLocalize(const UClass* Class) const { if (const UAssetDefinition* AssetDefinition = UAssetDefinitionRegistry::Get()->GetAssetDefinitionForClass(Class)) { return AssetDefinition->CanLocalize(FAssetData()).IsSupported(); } else { FAssetToolsModule& Module = FModuleManager::GetModuleChecked("AssetTools"); if (TSharedPtr AssetActions = Module.Get().GetAssetTypeActionsForClass(Class).Pin()) { return AssetActions->CanLocalize(); } } return false; } ELocalizedAssetsOnDiskResult FLocalizedAssetTools::GetLocalizedVariantsOnDisk(const TArray& InPackages, TMap>& OutLocalizedVariantsBySource, TArray* OutPackagesNotFound /*= nullptr*/) const { FScopedSlowTask GettingLocalizedVariantsOnDiskSlowTask(1.0f, LOCTEXT("GettingLocalizedVariantsOnDiskSlowTask", "Getting localized variants on disk...")); OutLocalizedVariantsBySource.Reserve(InPackages.Num()); if (OutPackagesNotFound != nullptr) { OutPackagesNotFound->Reserve(InPackages.Num()); } TMap PackagesToAssetDataMap; UE::AssetRegistry::GetAssetForPackages(InPackages, PackagesToAssetDataMap); if (!ensureAlwaysMsgf(PackagesToAssetDataMap.Num() == InPackages.Num(), TEXT("PackageNames were not properly converted to AssetData."))) { for (const FName& OriginalAssetName : InPackages) { OutLocalizedVariantsBySource.Add({ OriginalAssetName, TArray() }); } return ELocalizedAssetsOnDiskResult::PackageNamesError; } UAssetDefinitionRegistry* AssetDefinitionRegistry = UAssetDefinitionRegistry::Get(); float ProgressStep = 1.0f / static_cast(InPackages.Num()); for (const FName& OriginalAssetName : InPackages) { GettingLocalizedVariantsOnDiskSlowTask.EnterProgressFrame(ProgressStep); FString SourceAssetPathStr = OriginalAssetName.ToString(); FPackageLocalizationUtil::ConvertLocalizedToSource(SourceAssetPathStr, SourceAssetPathStr); const FName SourceAssetName(*SourceAssetPathStr); if (OutLocalizedVariantsBySource.Contains(SourceAssetName)) { continue; // We want to avoid doing any unnecessary work if it was already processed } // We want to avoid doing any unnecessary work on assets that does not require checking for variants FAssetData SourceAssetData = PackagesToAssetDataMap[OriginalAssetName]; UClass* SourceAssetClass = SourceAssetData.GetClass(); const UAssetDefinition* SourceAssetDefinition = AssetDefinitionRegistry->GetAssetDefinitionForClass(SourceAssetClass); bool bShouldCheckForVariant = SourceAssetDefinition != nullptr && SourceAssetDefinition->CanLocalize(SourceAssetData).IsSupported(); if (!bShouldCheckForVariant) { OutLocalizedVariantsBySource.Add({ SourceAssetName, TArray() }); continue; } // Check on disk for localized variants first. Remember the assets that had no variants on disk // because we will then check in Revision Control if applicable TArray LocalizedVariantsPaths; FPackageLocalizationUtil::GetLocalizedVariantsAbsolutePaths(SourceAssetPathStr, LocalizedVariantsPaths); if (LocalizedVariantsPaths.IsEmpty()) { if (OutPackagesNotFound != nullptr) { OutPackagesNotFound->Add(OriginalAssetName); } continue; } // If localized variants were found on disk, let's build renaming data for them too TArray LocalizedAssets; LocalizedAssets.Reserve(LocalizedVariantsPaths.Num()); for (const FString& LocalizedVariantPath : LocalizedVariantsPaths) { FString Culture; FPackageLocalizationUtil::ExtractCultureFromLocalized(LocalizedVariantPath, Culture); FString LocalizedAsset; FPackageLocalizationUtil::ConvertSourceToLocalized(SourceAssetPathStr, Culture, LocalizedAsset); LocalizedAssets.Add(FName(LocalizedAsset)); } OutLocalizedVariantsBySource.Add({ SourceAssetName, LocalizedAssets }); } return ELocalizedAssetsOnDiskResult::Success; } ELocalizedAssetsInSCCResult FLocalizedAssetTools::GetLocalizedVariantsInRevisionControl(const TArray& InPackages, TMap>& OutLocalizedVariantsBySource, TArray* OutPackagesNotFound /*= nullptr*/) const { FScopedSlowTask GetLocalizedVariantsInRevisionControlSlowTask(1.0f, LOCTEXT("GetLocalizedVariantsInRevisionControlSlowTask", "Querying Revision Control for localized variants... This could take a long time.")); GetLocalizedVariantsInRevisionControlSlowTask.EnterProgressFrame(0.05f); OutLocalizedVariantsBySource.Reserve(InPackages.Num()); // Modify data to be used by USourceControlHelpers TArray PackagesAsString; PackagesAsString.Reserve(InPackages.Num()); for (const FName& InPackageName : InPackages) { PackagesAsString.Add(InPackageName.ToString()); } // Let's check the packages presence in Revision Control in a single query TArray LocalizedVariantsInRevisionControl; GetLocalizedVariantsInRevisionControlSlowTask.EnterProgressFrame(0.9f); bool bOutRevisionControlWasNeeded = !GetLocalizedVariantsDepotPaths(PackagesAsString, LocalizedVariantsInRevisionControl); // Fill a proper structure with the results float ProgressStep = 0.03f / static_cast(LocalizedVariantsInRevisionControl.Num()); for (const FString& LocalizedVariantInRevisionControl : LocalizedVariantsInRevisionControl) { GetLocalizedVariantsInRevisionControlSlowTask.EnterProgressFrame(ProgressStep); FString SourceAsset; FPackageLocalizationUtil::ConvertToSource(LocalizedVariantInRevisionControl, SourceAsset); FName SourceAssetName(SourceAsset); TArray* VariantsBySourceFound = OutLocalizedVariantsBySource.Find(SourceAssetName); TArray& VariantsBySource = (VariantsBySourceFound != nullptr ? *VariantsBySourceFound : OutLocalizedVariantsBySource.Add({ SourceAssetName, TArray() })); VariantsBySource.Add(FName(LocalizedVariantInRevisionControl)); } // Don't forget to return the information on the packages that found nothing in Revision Control if (OutPackagesNotFound != nullptr) { ProgressStep = 0.02f / static_cast(InPackages.Num()); for (const FName& PackageName : InPackages) { GetLocalizedVariantsInRevisionControlSlowTask.EnterProgressFrame(ProgressStep); FString SourcePackage; FPackageLocalizationUtil::ConvertToSource(PackageName.ToString(), SourcePackage); if (OutLocalizedVariantsBySource.Find(FName(SourcePackage)) == nullptr) { // Package not found OutPackagesNotFound->Add(PackageName); } } } return bOutRevisionControlWasNeeded ? ELocalizedAssetsInSCCResult::RevisionControlNotAvailable : ELocalizedAssetsInSCCResult::Success; } ELocalizedAssetsResult FLocalizedAssetTools::GetLocalizedVariants(const TArray& InPackages, TMap>& OutLocalizedVariantsBySourceOnDisk, bool bAlsoCheckInRevisionControl, TMap>& OutLocalizedVariantsBySourceInRevisionControl, TArray* OutPackagesNotFound /*= nullptr*/) const { ELocalizedAssetsResult Result = ELocalizedAssetsResult::Success; // Check on disk first // Call the LocalizedAssetTools interface to GetLocalizedVariantsOnDisk of a list of packages TMap> VariantsBySources; TArray VariantsMaybeInRevisionControl; ELocalizedAssetsOnDiskResult DiskResult = GetLocalizedVariantsOnDisk(InPackages, OutLocalizedVariantsBySourceOnDisk, bAlsoCheckInRevisionControl ? &VariantsMaybeInRevisionControl : OutPackagesNotFound); Result = (DiskResult == ELocalizedAssetsOnDiskResult::PackageNamesError ? ELocalizedAssetsResult::PackageNamesError : Result); // Check in Revision Control if applicable // Call the LocalizedAssetTools interface to GetLocalizedVariantsInRevisionControl of a list of packages bool bRevisionControlWasNeeded = false; if (!VariantsMaybeInRevisionControl.IsEmpty()) { if (Result == ELocalizedAssetsResult::Success) { ELocalizedAssetsInSCCResult SCCResult = GetLocalizedVariantsInRevisionControl(VariantsMaybeInRevisionControl, OutLocalizedVariantsBySourceInRevisionControl, OutPackagesNotFound); Result = (SCCResult == ELocalizedAssetsInSCCResult::RevisionControlNotAvailable ? ELocalizedAssetsResult::RevisionControlNotAvailable : Result); } else { OutPackagesNotFound->Append(VariantsMaybeInRevisionControl); } } return Result; } void FLocalizedAssetTools::OpenRevisionControlRequiredDialog() const { FText WarningText = RevisionControlIsNotAvailableWarningText; const FText AvoidWarningText = LOCTEXT("HowToFixRevisionControlIsRequiredToManageLocalizableAssets", "If you want to disable this project option, it is located under:\n\tProject Settings/\n\tEditor/\n\tRevision Control/\n\tRequires Revision Control To Manage Localizable Assets\n\nThis option is there to prevent breaking paths between a source asset and its localized variants if they are not on disk."); FMessageDialog::Open(EAppMsgType::Ok, WarningText.Format(LOCTEXT("RevisionControlIsRequiredToManageLocalizableAssetsDialog", "{0}\n\n{1}"), WarningText, AvoidWarningText)); } void FLocalizedAssetTools::OpenFilesInRevisionControlRequiredDialog(const TArray& FileList) const { OpenLocalizedVariantsListMessageDialog(FilesNeedToBeOnDiskWarningText, LOCTEXT("FilesToSyncDialogHeader", "The following assets were found only in Revision Control. They need to be on your disk to be renamed."), FileList); } void FLocalizedAssetTools::OpenLocalizedVariantsListMessageDialog(const FText& Header, const FText& Message, const TArray& FileList) const { SFileListReportDialog::OpenListDialog(Header, Message, FileList, true); } ELocalizedVariantsInclusion FLocalizedAssetTools::OpenIncludeLocalizedVariantsListDialog(const TArray& FileList, ELocalizedVariantsInclusion RecommendedBehavior /*= ELocalizedVariantsInclusion::Include*/, bool bAllowOperationCanceling /*= true*/, ELocalizedVariantsInclusion UnattendedDefaultBehavior /*= ELocalizedVariantsInclusion::Exclude*/) const { return SIncludeLocalizedVariantsDialog::OpenIncludeListDialog(LOCTEXT("IncludeLocalizedVariantsDialogTitle", "Include Localized Variants"), LOCTEXT("IncludeLocalizedVariantsDialogHeader", "The current operation could also apply to the following localized variants (or source asset). Do you want to include them in the current operation ?"), FileList, RecommendedBehavior, bAllowOperationCanceling, UnattendedDefaultBehavior); } const FText& FLocalizedAssetTools::GetRevisionControlIsNotAvailableWarningText() const { return RevisionControlIsNotAvailableWarningText; } const FText& FLocalizedAssetTools::GetFilesNeedToBeOnDiskWarningText() const { return FilesNeedToBeOnDiskWarningText; } bool FLocalizedAssetTools::GetLocalizedVariantsDepotPaths(const TArray& InPackagesNames, TArray& OutLocalizedVariantsPaths) const { // Ensure source control system is up and running with a configured provider ISourceControlModule& SCModule = ISourceControlModule::Get(); if (!SCModule.IsEnabled()) { return false; } ISourceControlProvider& Provider = SCModule.GetProvider(); if (!Provider.IsAvailable()) { return false; } // Other providers don't work for now if (Provider.GetName() == "Perforce") { TArray LocalizedVariantsRegexPaths; LocalizedVariantsRegexPaths.Reserve(InPackagesNames.Num()); for (const FString& InPackageName : InPackagesNames) { FString SourcePackageName; FPackageLocalizationUtil::ConvertToSource(InPackageName, SourcePackageName); FString LocalizedVariantsRegexPath; FPackageLocalizationUtil::ConvertSourceToRegexLocalized(SourcePackageName, LocalizedVariantsRegexPath); LocalizedVariantsRegexPath += FPackageName::GetAssetPackageExtension(); LocalizedVariantsRegexPaths.Add(LocalizedVariantsRegexPath); } bool bSilent = true; bool bIncludeDeleted = true; USourceControlHelpers::GetFilesInDepotAtPaths(LocalizedVariantsRegexPaths, OutLocalizedVariantsPaths, bIncludeDeleted, bSilent, true); } return true; } #undef LOCTEXT_NAMESPACE