// Copyright Epic Games, Inc. All Rights Reserved. #include "LevelEditorOutlinerSettings.h" #include "Filters/FilterBase.h" #include "Filters/CustomClassFilterData.h" #include "Filters/GenericFilter.h" #include "LevelEditor.h" #include "IPlacementModeModule.h" #include "UnsavedAssetsTrackerModule.h" #include "UncontrolledChangelistsModule.h" #include "SceneOutlinerPublicTypes.h" #include "ISceneOutliner.h" #include "SceneOutlinerHelpers.h" #include "Animation/SkeletalMeshActor.h" #include "AssetRegistry/AssetRegistryModule.h" #include "Components/SkyAtmosphereComponent.h" #include "Components/VolumetricCloudComponent.h" #include "Engine/LocalFogVolume.h" #include "Engine/Blueprint.h" #include "Engine/Brush.h" #include "Engine/ExponentialHeightFog.h" #include "Engine/PostProcessVolume.h" #include "Engine/StaticMeshActor.h" #include "InstancedFoliageActor.h" #include "Landscape.h" #include "LevelInstance/LevelInstanceActor.h" #include "Sound/AmbientSound.h" #include "SourceControlHelpers.h" #define LOCTEXT_NAMESPACE "LevelEditorOutlinerSettings" const FString FLevelEditorOutlinerSettings::UnsavedAssetsFilterName("UnsavedAssetsFilter"); const FString FLevelEditorOutlinerSettings::UncontrolledAssetsFilterName("UncontrolledAssetsFilter"); FLevelEditorOutlinerSettings::~FLevelEditorOutlinerSettings() { if (IAssetRegistry* AssetRegistry = IAssetRegistry::Get()) { AssetRegistry->OnFilesLoaded().RemoveAll(this); } } void FLevelEditorOutlinerSettings::Initialize() { if (!GIsEditor) // to avoid crashes in server builds as CreateDefaultFilters will force loading the PlacementMode Module { return; } FDelayedAutoRegisterHelper EndOfEngineInit(EDelayedRegisterRunPhase::EndOfEngineInit, [WeakThis = AsWeak()]() { if (const TSharedPtr This = WeakThis.Pin()) { // Ensure all the CustomClassFilters are up to date by rerunning CreateDefaultFilters when the asset registry is loaded // This is to ensure that FPlacementModeModule can access all placeable classes during the call to CreateDefaultFilters IAssetRegistry& AssetRegistry = FModuleManager::LoadModuleChecked(TEXT("AssetRegistry")).Get(); AssetRegistry.OnFilesLoaded().AddSPLambda(This.Get(), [WeakThis]() { if (const TSharedPtr This = WeakThis.Pin()) { IAssetRegistry::GetChecked().OnFilesLoaded().RemoveAll(This.Get()); This->CreateDefaultFilters(); } }); } }); } void FLevelEditorOutlinerSettings::AddCustomFilter(TSharedRef> InCustomFilter) { CustomFilters.Add(InCustomFilter); } void FLevelEditorOutlinerSettings::AddCustomFilter(FOutlinerFilterFactory InCreateCustomFilter) { check (InCreateCustomFilter.IsBound()) CustomFilterDelegates.Add(InCreateCustomFilter); } void FLevelEditorOutlinerSettings::AddCustomClassFilter(TSharedRef InCustomClassFilterData) { TSharedRef* FoundFilter = CustomClassFilters.FindByPredicate([&InCustomClassFilterData](const TSharedPtr FilterData) { return FilterData->GetClassPathName() == InCustomClassFilterData->GetClassPathName(); }); // If a filter for the class already exists, just add the new categories to it // TODO: Not perfect, what if the user wants to use InCustomClassFilterData after if there is already a dupe? if (FoundFilter) { TArray> Categories = InCustomClassFilterData->GetCategories(); for(const TSharedPtr& Category : Categories) { (*FoundFilter)->AddCategory(Category); } } else { CustomClassFilters.Add(InCustomClassFilterData); } } TSharedPtr FLevelEditorOutlinerSettings::GetFilterCategory(const FName& CategoryName) { if (TSharedPtr* FoundCategory = FilterBarCategories.Find(CategoryName)) { return *FoundCategory; } return nullptr; } void FLevelEditorOutlinerSettings::SetupBuiltInCategories() { // First setup our unique built in categories TSharedPtr CommonFiltersCategory = MakeShared(LOCTEXT("CommonFiltersCategory", "Common"), FText::GetEmpty()); FilterBarCategories.Add(FLevelEditorOutlinerBuiltInCategories::Common(), CommonFiltersCategory); TSharedPtr AudioFiltersCategory = MakeShared(LOCTEXT("AudioFiltersCategory", "Audio"), FText::GetEmpty()); FilterBarCategories.Add(FLevelEditorOutlinerBuiltInCategories::Audio(), AudioFiltersCategory); TSharedPtr AnimationFiltersCategory = MakeShared(LOCTEXT("AnimationFiltersCategory", "Animation"), FText::GetEmpty()); FilterBarCategories.Add(FLevelEditorOutlinerBuiltInCategories::Animation(), AnimationFiltersCategory); TSharedPtr GeometryFiltersCategory = MakeShared(LOCTEXT("GeometryFiltersCategory", "Geometry"), FText::GetEmpty()); FilterBarCategories.Add(FLevelEditorOutlinerBuiltInCategories::Geometry(), GeometryFiltersCategory); TSharedPtr EnvironmentFiltersCategory = MakeShared(LOCTEXT("EnvironmentFiltersCategory", "Environment"), FText::GetEmpty()); FilterBarCategories.Add(FLevelEditorOutlinerBuiltInCategories::Environment(), EnvironmentFiltersCategory); TSharedPtr VPFiltersCategory = MakeShared(LOCTEXT("VPFiltersCategory", "Virtual Production"), FText::GetEmpty()); FilterBarCategories.Add(FLevelEditorOutlinerBuiltInCategories::VirtualProduction(), VPFiltersCategory); TSharedPtr SCCFiltersCategory = MakeShared(LOCTEXT("SCCFiltersCategory", "Revision Control"), FText::GetEmpty()); FilterBarCategories.Add(FLevelEditorOutlinerBuiltInCategories::SourceControl(), SCCFiltersCategory); // Now convert some of the built in placement mode categories we want to filter categories and add them PlacementToFilterCategoryMap.Add(FBuiltInPlacementCategories::Basic(), FLevelEditorOutlinerBuiltInCategories::Basic()); TSharedPtr BasicFilterCategory = MakeShared(LOCTEXT("BasicFilterCategory", "Basic"), FText::GetEmpty()); FilterBarCategories.Add(FLevelEditorOutlinerBuiltInCategories::Basic(), BasicFilterCategory); PlacementToFilterCategoryMap.Add(FBuiltInPlacementCategories::Lights(), FLevelEditorOutlinerBuiltInCategories::Lights()); TSharedPtr LightsFilterCategory = MakeShared(LOCTEXT("LightsFilterCategory", "Lights"), FText::GetEmpty()); FilterBarCategories.Add(FLevelEditorOutlinerBuiltInCategories::Lights(), LightsFilterCategory); PlacementToFilterCategoryMap.Add(FBuiltInPlacementCategories::Visual(), FLevelEditorOutlinerBuiltInCategories::Visual()); TSharedPtr VisualFilterCategory = MakeShared(LOCTEXT("VisualFilterCategory", "Visual"), FText::GetEmpty()); FilterBarCategories.Add(FLevelEditorOutlinerBuiltInCategories::Visual(), VisualFilterCategory); PlacementToFilterCategoryMap.Add(FBuiltInPlacementCategories::Volumes(), FLevelEditorOutlinerBuiltInCategories::Volumes()); TSharedPtr VolumesFilterCategory = MakeShared(LOCTEXT("VolumnesFilterCategory", "Volumes"), FText::GetEmpty()); FilterBarCategories.Add(FLevelEditorOutlinerBuiltInCategories::Volumes(), VolumesFilterCategory); } void FLevelEditorOutlinerSettings::RefreshOutlinersWithActiveFilter(bool bFullRefresh, const FString& InFilterName) { TRACE_CPUPROFILER_EVENT_SCOPE(FLevelEditorOutlinerSettings::RefreshOutlinersWithActiveFilter); TWeakPtr LevelEditor = FModuleManager::GetModuleChecked(TEXT("LevelEditor")).GetLevelEditorInstance(); if (LevelEditor.IsValid()) { TArray> SceneOutlinerPtrs = LevelEditor.Pin()->GetAllSceneOutliners(); for (TWeakPtr SceneOutlinerPtr : SceneOutlinerPtrs) { if (TSharedPtr SceneOutlinerPin = SceneOutlinerPtr.Pin()) { // If this outliner has the input filter active, refresh it if (SceneOutlinerPin->IsFilterActive(InFilterName)) { if (bFullRefresh) { SceneOutlinerPin->FullRefresh(); } else { SceneOutlinerPin->Refresh(); } } } } } } void FLevelEditorOutlinerSettings::CreateDefaultFilters() { // First we will add all items registered to the place actors panel as filters IPlacementModeModule& PlacementModeModule = IPlacementModeModule::Get(); // Get all the Categories TArray Categories; PlacementModeModule.GetSortedCategories(Categories); // Remove the Recently Placed, Shapes (don't make sense) and All Classes (too much bloat) categories from the filters Categories.RemoveAll([](const FPlacementCategoryInfo& Category) { return Category.UniqueHandle == FBuiltInPlacementCategories::RecentlyPlaced() || Category.UniqueHandle == FBuiltInPlacementCategories::AllClasses() || Category.UniqueHandle == FBuiltInPlacementCategories::Shapes(); }); auto FindOrAddClassFilter = [this](UClass* Class, TSharedPtr FilterCategory) { // If the underlying class already exists, just add this category to it TSharedRef* ExistingClassData = CustomClassFilters.FindByPredicate([Class](const TSharedPtr FilterData) { return FilterData->GetClassPathName() == Class->GetClassPathName(); }); if (ExistingClassData) { (*ExistingClassData)->AddCategory(FilterCategory); } else { TSharedRef NewClassData = MakeShared(Class, FilterCategory, FLinearColor::White); CustomClassFilters.Add(NewClassData); } }; for (const FPlacementCategoryInfo& Category : Categories) { // Make an FFilterCategory using the current PlacementCategory if it doesn't already exist (built in) TSharedPtr FilterCategory; if (TSharedPtr* FoundCategory = FilterBarCategories.Find(Category.UniqueHandle)) { FilterCategory = *FoundCategory; } else { FilterCategory = MakeShared(Category.DisplayName, FText::GetEmpty()); } FName CategoryName = Category.UniqueHandle; // If it is a built in placement mode category, convert the name to a built in filter category if (FName* MappedCategoryName = PlacementToFilterCategoryMap.Find(CategoryName)) { CategoryName = *MappedCategoryName; } // Add the category to our list FilterBarCategories.Add(Category.UniqueHandle, FilterCategory); // Get all the items belonging to the current category TArray> Items; PlacementModeModule.RegenerateItemsForCategory(Category.UniqueHandle); PlacementModeModule.GetItemsForCategory(Category.UniqueHandle, Items); // Add each item to as a filter for(TSharedPtr& Item : Items) { // Get the underlying class from the Actor belonging to this item const bool bIsClass = Item->AssetData.GetClass() == UClass::StaticClass(); const bool bIsActor = bIsClass ? CastChecked(Item->AssetData.GetAsset())->IsChildOf(AActor::StaticClass()) : false; const AActor* DefaultActor = nullptr; if (Item->Factory != nullptr) { DefaultActor = Item->Factory->GetDefaultActor(Item->AssetData); } else if (bIsActor) { DefaultActor = CastChecked(CastChecked(Item->AssetData.GetAsset())->GetDefaultObject(false)); } if (!DefaultActor) { continue; } UClass* Class = DefaultActor->GetClass(); FindOrAddClassFilter(Class, FilterCategory); } } // Now we add some custom filterable types that are not a part of the place actors panel TSharedPtr* CommonFilterCategory = FilterBarCategories.Find(FLevelEditorOutlinerBuiltInCategories::Common()); if (CommonFilterCategory) { FindOrAddClassFilter(AStaticMeshActor::StaticClass(), *CommonFilterCategory); FindOrAddClassFilter(ALevelInstance::StaticClass(), *CommonFilterCategory); FindOrAddClassFilter(ASkeletalMeshActor::StaticClass(), *CommonFilterCategory); FindOrAddClassFilter(UBlueprint::StaticClass(), *CommonFilterCategory); FindOrAddClassFilter(APostProcessVolume::StaticClass(), *CommonFilterCategory); } TSharedPtr* GeometryFilterCategory = FilterBarCategories.Find(FLevelEditorOutlinerBuiltInCategories::Geometry()); if (GeometryFilterCategory) { FindOrAddClassFilter(AStaticMeshActor::StaticClass(), *GeometryFilterCategory); FindOrAddClassFilter(ABrush::StaticClass(), *GeometryFilterCategory); } TSharedPtr* AnimationFilterCategory = FilterBarCategories.Find(FLevelEditorOutlinerBuiltInCategories::Animation()); if (AnimationFilterCategory) { FindOrAddClassFilter(ASkeletalMeshActor::StaticClass(), *AnimationFilterCategory); } TSharedPtr* EnvironmentFilterCategory = FilterBarCategories.Find(FLevelEditorOutlinerBuiltInCategories::Environment()); if (EnvironmentFilterCategory) { FindOrAddClassFilter(AExponentialHeightFog::StaticClass(), *EnvironmentFilterCategory); FindOrAddClassFilter(AInstancedFoliageActor::StaticClass(), *EnvironmentFilterCategory); FindOrAddClassFilter(ASkyAtmosphere::StaticClass(), *EnvironmentFilterCategory); FindOrAddClassFilter(ALocalFogVolume::StaticClass(), *EnvironmentFilterCategory); FindOrAddClassFilter(AVolumetricCloud::StaticClass(), *EnvironmentFilterCategory); } TSharedPtr* AudioFilterCategory = FilterBarCategories.Find(FLevelEditorOutlinerBuiltInCategories::Audio()); if (AudioFilterCategory) { FindOrAddClassFilter(AAmbientSound::StaticClass(), *AudioFilterCategory); } // Destroy and recreate our built in custom filters BuiltInCustomFilters.Empty(); CreateSCCFilters(); } bool FLevelEditorOutlinerSettings::DoesActorPassUnsavedFilter(const ISceneOutlinerTreeItem& InItem) { return UnsavedPackages.Contains(*InItem.GetPackageName()); } bool FLevelEditorOutlinerSettings::DoesActorPassUncontrolledFilter(const ISceneOutlinerTreeItem& InItem) { return UncontrolledPackages.Contains(*InItem.GetPackageName()); } void FLevelEditorOutlinerSettings::OnUnsavedAssetAdded(const FString& InAsset) { FNameBuilder PackageName; if (FPackageName::TryConvertFilenameToLongPackageName(InAsset, PackageName)) { bool bAlreadyInSet = false; UnsavedPackages.Add(FName(PackageName), &bAlreadyInSet); if (!bAlreadyInSet) { // Refresh any outliners that have the unsaved assets filter active to refilter on adding an unsaved asset RefreshOutlinersWithActiveFilter(/*bFullRefresh*/ true, UnsavedAssetsFilterName); } } } void FLevelEditorOutlinerSettings::OnUnsavedAssetRemoved(const FString& InAsset) { FNameBuilder PackageName; if (FPackageName::TryConvertFilenameToLongPackageName(InAsset, PackageName)) { if (UnsavedPackages.Remove(FName(PackageName)) > 0) { // Refresh any outliners that have the unsaved assets filter active to refilter on adding an unsaved asset RefreshOutlinersWithActiveFilter(/*bFullRefresh*/ true, UnsavedAssetsFilterName); } } } void FLevelEditorOutlinerSettings::CreateSCCFilters() { // Source Control Category TSharedPtr SCCFiltersCategory = GetFilterCategory(FLevelEditorOutlinerBuiltInCategories::SourceControl()); // Uncontrolled Actors Filter FUncontrolledChangelistsModule::Get().OnUncontrolledChangelistModuleChanged.AddSP(this, &FLevelEditorOutlinerSettings::OnUncontrolledChangelistModuleChanged); OnUncontrolledChangelistModuleChanged(); FGenericFilter::FOnItemFiltered UncontrolledFilterDelegate = FGenericFilter::FOnItemFiltered::CreateSP(this, &FLevelEditorOutlinerSettings::DoesActorPassUncontrolledFilter); TSharedPtr> UncontrolledFilter = MakeShared>(SCCFiltersCategory, UncontrolledAssetsFilterName, LOCTEXT("UncontrolledFilterName", "Uncontrolled"), UncontrolledFilterDelegate); UncontrolledFilter->SetToolTipText(LOCTEXT("UncontrolledFilterTooltip", "Only show items that are uncontrolled (locally modified outside of revision control)")); BuiltInCustomFilters.Add(UncontrolledFilter.ToSharedRef()); // File Management Category TSharedPtr FileManagementFiltersCategory = MakeShared(LOCTEXT("FileManagementFiltersCategory", "File Management"), FText::GetEmpty()); // Unsaved Actors FUnsavedAssetsTrackerModule& UnsavedAssetsTrackerModule = FUnsavedAssetsTrackerModule::Get(); // Update our cached unsaved packages { UnsavedPackages.Reset(); FNameBuilder PackageName; for (const FString& Filename : UnsavedAssetsTrackerModule.GetUnsavedAssets()) { if (FPackageName::TryConvertFilenameToLongPackageName(Filename, PackageName)) { UnsavedPackages.Add(FName(PackageName)); } } } UnsavedAssetsTrackerModule.OnUnsavedAssetAdded.AddSP(this, &FLevelEditorOutlinerSettings::OnUnsavedAssetAdded); UnsavedAssetsTrackerModule.OnUnsavedAssetRemoved.AddSP(this, &FLevelEditorOutlinerSettings::OnUnsavedAssetRemoved); FGenericFilter::FOnItemFiltered UnsavedActorsFilterDelegate = FGenericFilter::FOnItemFiltered::CreateSP(this, & FLevelEditorOutlinerSettings::DoesActorPassUnsavedFilter); TSharedPtr> UnsavedAssetsFilter = MakeShared>(FileManagementFiltersCategory, UnsavedAssetsFilterName, LOCTEXT("UnsavedFilterName", "Unsaved"), UnsavedActorsFilterDelegate); UnsavedAssetsFilter->SetToolTipText(LOCTEXT("UnsavedAssetsFilterTooltip", "Only show items that are unsaved")); BuiltInCustomFilters.Add(UnsavedAssetsFilter.ToSharedRef()); } void FLevelEditorOutlinerSettings::OnUncontrolledChangelistModuleChanged() { // Update our cached uncontrolled packages { UncontrolledPackages.Reset(); FNameBuilder PackageName; for (const TSharedRef& UncontrolledChangelistState : FUncontrolledChangelistsModule::Get().GetChangelistStates()) { for (const FString& Filename : UncontrolledChangelistState->GetFilenames()) { if (FPackageName::TryConvertFilenameToLongPackageName(Filename, PackageName)) { UncontrolledPackages.Add(FName(PackageName)); } } } } // Refresh any Outliners that have the Uncontrolled Filter active RefreshOutlinersWithActiveFilter(/*bFullRefresh*/ true, UncontrolledAssetsFilterName); } void FLevelEditorOutlinerSettings::GetOutlinerFilters(FSceneOutlinerFilterBarOptions& OutFilterBarOptions) { // Sort the type filters by name CustomClassFilters.Sort([](const TSharedRef& ClassA, const TSharedRef& ClassB) { return ClassA->GetName().CompareTo(ClassB->GetName()) < 0; }); OutFilterBarOptions.CustomClassFilters.Append(CustomClassFilters); OutFilterBarOptions.CustomFilters.Append(CustomFilters); OutFilterBarOptions.CustomFilters.Append(BuiltInCustomFilters); for(FOutlinerFilterFactory& CreateFilterDelegate : CustomFilterDelegates) { TSharedRef> NewFilter = CreateFilterDelegate.Execute().ToSharedRef(); OutFilterBarOptions.CustomFilters.Add(NewFilter); } } #undef LOCTEXT_NAMESPACE