// Copyright Epic Games, Inc. All Rights Reserved. #include "ActorDetails.h" #include "Engine/EngineBaseTypes.h" #include "Engine/Level.h" #include "UObject/UnrealType.h" #include "GameFramework/Actor.h" #include "Modules/ModuleManager.h" #include "Misc/PackageName.h" #include "Misc/MessageDialog.h" #include "Widgets/SNullWidget.h" #include "Widgets/DeclarativeSyntaxSupport.h" #include "Widgets/SBoxPanel.h" #include "Framework/Application/SlateApplication.h" #include "Textures/SlateIcon.h" #include "Framework/Commands/UIAction.h" #include "Framework/Commands/UICommandList.h" #include "Widgets/Layout/SBorder.h" #include "Widgets/Images/SImage.h" #include "Widgets/Text/STextBlock.h" #include "Widgets/Layout/SBox.h" #include "ToolMenus.h" #include "Widgets/Input/SButton.h" #include "Widgets/Input/SComboButton.h" #include "Styling/AppStyle.h" #include "Engine/Blueprint.h" #include "Engine/Brush.h" #include "Editor/UnrealEdEngine.h" #include "GameFramework/Volume.h" #include "GameFramework/WorldSettings.h" #include "Components/BillboardComponent.h" #include "Engine/BlueprintGeneratedClass.h" #include "Engine/Selection.h" #include "EditorModeManager.h" #include "EditorModes.h" #include "UnrealEdGlobals.h" #include "DetailLayoutBuilder.h" #include "DetailWidgetRow.h" #include "DetailCategoryBuilder.h" #include "IDetailsView.h" #include "LayersModule.h" #include "LevelEditor.h" #include "ClassViewerModule.h" #include "ClassViewerFilter.h" #include "Kismet2/KismetEditorUtilities.h" #include "EdGraphSchema_K2.h" #include "ComponentTransformDetails.h" #include "Widgets/SToolTip.h" #include "IDocumentation.h" #include "Engine/BrushShape.h" #include "ActorDetailsDelegates.h" #include "EditorCategoryUtils.h" #include "Widgets/Input/SHyperlink.h" #include "ObjectEditorUtils.h" #include "Misc/MessageDialog.h" #include "ScopedTransaction.h" #include "WorldPartition/WorldPartitionSubsystem.h" #include "WorldPartition/WorldPartition.h" #include "Algo/AnyOf.h" #include "Kismet2/BlueprintEditorUtils.h" #include "K2Node_AddDelegate.h" #include "EdGraphSchema_K2_Actions.h" #include "Subsystems/EditorActorSubsystem.h" #define LOCTEXT_NAMESPACE "ActorDetails" FExtendActorDetails OnExtendActorDetails; TSharedRef FActorDetails::MakeInstance() { return MakeShared(); } FActorDetails::~FActorDetails() { } void FActorDetails::CustomizeDetails( IDetailLayoutBuilder& DetailLayout ) { // Get the list of hidden categories TArray HideCategories; if (DetailLayout.GetBaseClass()) { FEditorCategoryUtils::GetClassHideCategories(DetailLayout.GetBaseClass(), HideCategories); } // These details only apply when adding an instance of the actor in a level if (TSharedPtr DetailsView = DetailLayout.GetDetailsViewSharedPtr(); !DetailLayout.HasClassDefaultObject() && DetailsView && DetailsView->GetSelectedActorInfo().NumSelected > 0) { // Build up a list of unique blueprints in the selection set (recording the first actor in the set for each one) TMap UniqueBlueprints; // Per level Actor Counts TMap ActorsPerLevelCount; bool bHasBillboardComponent = false; const TArray< TWeakObjectPtr >& SelectedObjects = DetailLayout.GetSelectedObjects(); for (int32 ObjectIndex = 0; ObjectIndex < SelectedObjects.Num(); ++ObjectIndex) { AActor* Actor = Cast( SelectedObjects[ObjectIndex].Get() ); if (Actor != NULL) { // Store the selected actors for use later. Its fine to do this when CustomizeDetails is called because if the selected actors changes, CustomizeDetails will be called again on a new instance // and our current resource would be destroyed. SelectedActors.Add( Actor ); // Record the level that contains this actor and increment it's actor count ULevel* Level = Actor->GetLevel(); if (Level != NULL) { int32& ActorCountForThisLevel = ActorsPerLevelCount.FindOrAdd(Level); ++ActorCountForThisLevel; } // Add to the unique blueprint map if the actor is generated from a blueprint if (UBlueprint* Blueprint = Cast(Actor->GetClass()->ClassGeneratedBy)) { if (!UniqueBlueprints.Find(Blueprint)) { UniqueBlueprints.Add(Blueprint, Actor); } } if (!bHasBillboardComponent) { bHasBillboardComponent = Actor->FindComponentByClass() != NULL; } } } if (!bHasBillboardComponent) { // Actor billboard scale is not relevant if the actor doesn't have a billboard component DetailLayout.HideProperty( GET_MEMBER_NAME_CHECKED(AActor, SpriteScale) ); } if (!HideCategories.Contains(TEXT("Transform"))) { AddTransformCategory(DetailLayout); } if (!HideCategories.Contains(TEXT("Actor"))) { AddActorCategory(DetailLayout, ActorsPerLevelCount); } // Hide World Partition specific properties in non WP levels const bool bShouldDisplayWorldPartitionProperties = Algo::AnyOf(SelectedActors, [](const TWeakObjectPtr Actor) { UWorld* World = Actor.IsValid() ? Actor->GetTypedOuter() : nullptr; return UWorld::IsPartitionedWorld(World); }); if (!bShouldDisplayWorldPartitionProperties) { DetailLayout.HideProperty(DetailLayout.GetProperty(AActor::GetRuntimeGridPropertyName(), AActor::StaticClass())); DetailLayout.HideProperty(DetailLayout.GetProperty(AActor::GetDataLayerAssetsPropertyName(), AActor::StaticClass())); DetailLayout.HideProperty(DetailLayout.GetProperty(AActor::GetDataLayerPropertyName(), AActor::StaticClass())); DetailLayout.HideProperty(DetailLayout.GetProperty(AActor::GetHLODLayerPropertyName(), AActor::StaticClass())); } OnExtendActorDetails.Broadcast(DetailLayout, FGetSelectedActors::CreateSP(this, &FActorDetails::GetSelectedActors)); } TSharedPtr PrimaryTickProperty = DetailLayout.GetProperty(GET_MEMBER_NAME_CHECKED(AActor, PrimaryActorTick)); // Defaults only show tick properties if (DetailLayout.HasClassDefaultObject()) { if (!HideCategories.Contains(TEXT("Tick"))) { // Note: the category is renamed to differentiate between IDetailCategoryBuilder& TickCategory = DetailLayout.EditCategory("Tick", LOCTEXT("TickCategoryName", "Actor Tick") ); TickCategory.AddProperty(PrimaryTickProperty->GetChildHandle(GET_MEMBER_NAME_CHECKED(FTickFunction, bStartWithTickEnabled))); TickCategory.AddProperty(PrimaryTickProperty->GetChildHandle(GET_MEMBER_NAME_CHECKED(FTickFunction, TickInterval))); TickCategory.AddProperty(PrimaryTickProperty->GetChildHandle(GET_MEMBER_NAME_CHECKED(FTickFunction, bTickEvenWhenPaused)), EPropertyLocation::Advanced); TickCategory.AddProperty(PrimaryTickProperty->GetChildHandle(GET_MEMBER_NAME_CHECKED(FTickFunction, bAllowTickOnDedicatedServer)), EPropertyLocation::Advanced); TickCategory.AddProperty(PrimaryTickProperty->GetChildHandle(GET_MEMBER_NAME_CHECKED(FTickFunction, TickGroup)), EPropertyLocation::Advanced); } if (!HideCategories.Contains(TEXT("Events"))) { AddEventsCategory(DetailLayout); } } PrimaryTickProperty->MarkHiddenByCustomization(); } void FActorDetails::OnConvertActor(UClass* ChosenClass) { if (ChosenClass) { // Check each selected actor's pointer. TArray SelectedActorsRaw; for (int32 i=0; i(), true); } } } class FConvertToClassFilter : public IClassViewerFilter { public: /** All classes in this set will be allowed. */ TSet< const UClass* > AllowedClasses; /** All classes in this set will be disallowed. */ TSet< const UClass* > DisallowedClasses; /** Allowed ChildOf relationship. */ TSet< const UClass* > AllowedChildOfRelationship; virtual bool IsClassAllowed(const FClassViewerInitializationOptions& InInitOptions, const UClass* InClass, TSharedRef< FClassViewerFilterFuncs > InFilterFuncs ) override { EFilterReturn::Type eState = InFilterFuncs->IfInClassesSet(AllowedClasses, InClass); if(eState == EFilterReturn::NoItems) { eState = InFilterFuncs->IfInChildOfClassesSet(AllowedChildOfRelationship, InClass); } // As long as it has not failed to be on an allowed list, check if it is on a disallowed list. if(eState == EFilterReturn::Passed) { eState = InFilterFuncs->IfInClassesSet(DisallowedClasses, InClass); // If it passes, it's on the disallowed list, so we do not want it. if(eState == EFilterReturn::Passed) { return false; } else { return true; } } return false; } virtual bool IsUnloadedClassAllowed(const FClassViewerInitializationOptions& InInitOptions, const TSharedRef< const IUnloadedBlueprintData > InUnloadedClassData, TSharedRef< FClassViewerFilterFuncs > InFilterFuncs) override { EFilterReturn::Type eState = InFilterFuncs->IfInClassesSet(AllowedClasses, InUnloadedClassData); if(eState == EFilterReturn::NoItems) { eState = InFilterFuncs->IfInChildOfClassesSet(AllowedChildOfRelationship, InUnloadedClassData); } // As long as it has not failed to be on an allowed list, check if it is on a disallowed list. if(eState == EFilterReturn::Passed) { eState = InFilterFuncs->IfInClassesSet(DisallowedClasses, InUnloadedClassData); // If it passes, it's on the disallowed list, so we do not want it. if(eState == EFilterReturn::Passed) { return false; } else { return true; } } return false; } }; UClass* FActorDetails::GetConversionRoot( UClass* InCurrentClass ) const { UClass* ParentClass = InCurrentClass; while(ParentClass) { if( ParentClass->GetBoolMetaData(FName(TEXT("IsConversionRoot"))) ) { break; } ParentClass = ParentClass->GetSuperClass(); } return ParentClass; } void FActorDetails::CreateClassPickerConvertActorFilter(const TWeakObjectPtr ConvertActor, class FClassViewerInitializationOptions* ClassPickerOptions) { // Shouldn't ever be overwriting an already established filter check( ConvertActor.IsValid() ) check( ClassPickerOptions != nullptr && ClassPickerOptions->ClassFilters.IsEmpty() ); TSharedRef Filter = MakeShared(); ClassPickerOptions->ClassFilters.Add(Filter); UClass* ConvertClass = ConvertActor->GetClass(); UClass* RootConversionClass = GetConversionRoot(ConvertClass); if(RootConversionClass) { Filter->AllowedChildOfRelationship.Add(RootConversionClass); } // Never convert to the same class Filter->DisallowedClasses.Add(ConvertClass); if( ConvertActor->IsA() ) { // Volumes cannot be converted to brushes or brush shapes or the abstract type Filter->DisallowedClasses.Add(ABrush::StaticClass()); Filter->DisallowedClasses.Add(ABrushShape::StaticClass()); Filter->DisallowedClasses.Add(AVolume::StaticClass()); } } TSharedRef FActorDetails::OnGetConvertContent() { // Build a class picker widget // Fill in options FClassViewerInitializationOptions Options; Options.bShowUnloadedBlueprints = true; Options.bIsActorsOnly = true; Options.bIsPlaceableOnly = true; // All selected actors are of the same class, so just need to use one to generate the filter if ( SelectedActors.Num() > 0 ) { CreateClassPickerConvertActorFilter(SelectedActors.Top(), &Options); } Options.Mode = EClassViewerMode::ClassPicker; Options.DisplayMode = EClassViewerDisplayMode::ListView; TSharedRef ClassPicker = FModuleManager::LoadModuleChecked("ClassViewer").CreateClassViewer(Options, FOnClassPicked::CreateSP(this, &FActorDetails::OnConvertActor)); return SNew(SBox) .WidthOverride(280.f) .MaxDesiredHeight(500.f) [ ClassPicker ]; } EVisibility FActorDetails::GetConvertMenuVisibility() const { return EVisibility::Visible; } TSharedRef FActorDetails::MakeConvertMenu( const FSelectedActorInfo& SelectedActorInfo ) { UClass* RootConversionClass = GetConversionRoot(SelectedActorInfo.SelectionClass); return SNew(SComboButton) .ContentPadding(2.f) .IsEnabled(RootConversionClass != NULL) .Visibility(this, &FActorDetails::GetConvertMenuVisibility) .OnGetMenuContent(this, &FActorDetails::OnGetConvertContent) .ButtonContent() [ SNew(STextBlock) .Text(LOCTEXT("SelectAType", "Select a Type")) .Font(IDetailLayoutBuilder::GetDetailFont()) ]; } void FActorDetails::OnNarrowSelectionSetToSpecificLevel( TWeakObjectPtr LevelToNarrowInto ) { if (ULevel* RequiredLevel = LevelToNarrowInto.Get()) { // Remove any selected objects that aren't in the specified level TArray ActorsToDeselect; for ( TArray< TWeakObjectPtr >::TConstIterator Iter(SelectedActors); Iter; ++Iter) { if( (*Iter).IsValid() ) { AActor* Actor = (*Iter).Get(); if (!Actor->IsIn(RequiredLevel)) { ActorsToDeselect.Add(Actor); } } } for (TArray::TIterator DeselectIt(ActorsToDeselect); DeselectIt; ++DeselectIt) { AActor* Actor = *DeselectIt; GEditor->SelectActor(Actor, /*bSelected=*/ false, /*bNotify=*/ false); } // Tell the editor selection status was changed. GEditor->NoteSelectionChange(); } } bool FActorDetails::IsActorValidForLevelScript() const { AActor* Actor = GEditor->GetSelectedActors()->GetTop(); return FKismetEditorUtilities::IsActorValidForLevelScript(Actor); } FReply FActorDetails::FindSelectedActorsInLevelScript() { GUnrealEd->FindSelectedActorsInLevelScript(); return FReply::Handled(); }; bool FActorDetails::AreAnySelectedActorsInLevelScript() const { return GUnrealEd->AreAnySelectedActorsInLevelScript(); }; /** Util to create a menu for events we can add for the selected actor */ TSharedRef FActorDetails::MakeEventOptionsWidgetFromSelection() { UToolMenus* ToolMenus = UToolMenus::Get(); static const FName MenuName("DetailCustomizations.EventOptions"); if (!ToolMenus->IsMenuRegistered(MenuName)) { ToolMenus->RegisterMenu(MenuName); } FToolMenuContext Context; UToolMenu* Menu = ToolMenus->GenerateMenu(MenuName, Context); AActor* Actor = SelectedActors[0].Get(); FKismetEditorUtilities::AddLevelScriptEventOptionsForActor(Menu, SelectedActors[0], true, true, false); return ToolMenus->GenerateWidget(Menu); } void FActorDetails::AddLayersCategory( IDetailLayoutBuilder& DetailBuilder ) { if( !FModuleManager::Get().IsModuleLoaded( TEXT("Layers") ) ) { return; } FLayersModule& LayersModule = FModuleManager::LoadModuleChecked< FLayersModule >( TEXT("Layers") ); const FText LayerCategory = LOCTEXT("LayersCategory", "Layers"); DetailBuilder.EditCategory( "Layers", LayerCategory, ECategoryPriority::Uncommon ) .AddCustomRow( FText::GetEmpty() ) [ LayersModule.CreateLayerCloud( SelectedActors ) ]; } void FActorDetails::AddTransformCategory( IDetailLayoutBuilder& DetailBuilder ) { const FSelectedActorInfo& SelectedActorInfo = DetailBuilder.GetDetailsViewSharedPtr()->GetSelectedActorInfo(); bool bAreBrushesSelected = SelectedActorInfo.bHaveBrush; bool bIsOnlyWorldPropsSelected = SelectedActors.Num() == 1 && SelectedActors[0].IsValid() && SelectedActors[0]->IsA(); bool bLacksRootComponent = SelectedActors[0].IsValid() && (SelectedActors[0]->GetRootComponent()==NULL); // Don't show the Transform details if the only actor selected is world properties, or if they have no RootComponent if ( bIsOnlyWorldPropsSelected || bLacksRootComponent ) { return; } TSharedRef TransformDetails = MakeShared(DetailBuilder.GetSelectedObjects(), SelectedActorInfo, DetailBuilder); IDetailCategoryBuilder& TransformCategory = DetailBuilder.EditCategory( "TransformCommon", LOCTEXT("TransformCommonCategory", "Transform"), ECategoryPriority::Transform ); TransformCategory.AddCustomBuilder( TransformDetails ); } void FActorDetails::AddEventsCategory(IDetailLayoutBuilder& DetailBuilder) { // Get the currently selected actor, which would be the "Default__Actor" const TArray>& Selected = DetailBuilder.GetSelectedObjects(); if(Selected.IsEmpty()) { return; } AActor* Actor = Cast(Selected[0].Get()); UBlueprint* Blueprint = Actor ? Cast(Actor->GetClass()->ClassGeneratedBy) : nullptr; if(!Actor || !Blueprint || !FBlueprintEditorUtils::DoesSupportEventGraphs(Blueprint)) { return; } IDetailCategoryBuilder& EventsCategory = DetailBuilder.EditCategory("Events", FText::GetEmpty(), ECategoryPriority::Uncommon); static const FName HideInDetailPanelName("HideInDetailPanel"); // Find all the Multicast delegate properties and give a binding button for them for (TFieldIterator PropertyIt(Actor->GetClass(), EFieldIteratorFlags::IncludeSuper); PropertyIt; ++PropertyIt) { FMulticastDelegateProperty* Property = *PropertyIt; // Only show BP assiangable, non-hidden delegates if (!Property->HasAnyPropertyFlags(CPF_Parm) && Property->HasAllPropertyFlags(CPF_BlueprintAssignable) && !Property->HasMetaData(HideInDetailPanelName)) { const FName EventName = Property->GetFName(); FText EventText = Property->GetDisplayNameText(); EventsCategory.AddCustomRow(EventText) .WholeRowContent() [ SNew(SHorizontalBox) .ToolTipText(Property->GetToolTipText()) + SHorizontalBox::Slot() .AutoWidth() .VAlign(VAlign_Center) .Padding(0.0f, 0.0f, 5.0f, 0.0f) [ SNew(SImage) .Image(FAppStyle::GetBrush("GraphEditor.Event_16x")) ] + SHorizontalBox::Slot() .VAlign(VAlign_Center) [ SNew(STextBlock) .Font(IDetailLayoutBuilder::GetDetailFont()) .Text(EventText) ] + SHorizontalBox::Slot() .HAlign(HAlign_Left) .VAlign(VAlign_Center) .Padding(0.f) [ // A "Plus" button to add a binding. For dynamic delegates on the CDO, you can always // make a new binding, so always display the "Plus" SNew(SButton) .ContentPadding(FMargin(3.0f, 2.0f)) .HAlign(HAlign_Center) .OnClicked(this, &FActorDetails::HandleAddOrViewEventForVariable, Blueprint, Property) [ SNew(SImage) .ColorAndOpacity(FSlateColor::UseForeground()) .Image(FAppStyle::Get().GetBrush("Icons.Plus")) ] ] ]; } } } FReply FActorDetails::HandleAddOrViewEventForVariable(UBlueprint* BP, FMulticastDelegateProperty* Property) { const UFunction* SignatureFunction = Property ? Property->SignatureFunction : nullptr; UEdGraph* EventGraph = BP ? FBlueprintEditorUtils::FindEventGraph(BP) : nullptr; if (EventGraph && SignatureFunction && Property) { const FVector2D SpawnPos = EventGraph->GetGoodPlaceForNewNode(); // Adding a bound dynatic delegate from the Actor that is based off this BP will always be in a self context UK2Node_AddDelegate* TemplateNode = NewObject(); TemplateNode->SetFromProperty(Property, /* bSelfContext */ true, Property->GetOwnerClass()); UEdGraphNode* SpawnedDelegate = FEdGraphSchemaAction_K2AssignDelegate::AssignDelegate(TemplateNode, EventGraph, nullptr, SpawnPos, true); FKismetEditorUtilities::BringKismetToFocusAttentionOnObject(SpawnedDelegate, false); } return FReply::Handled(); } void FActorDetails::AddActorCategory( IDetailLayoutBuilder& DetailBuilder, const TMap& ActorsPerLevelCount ) { FLevelEditorModule& LevelEditor = FModuleManager::GetModuleChecked( TEXT("LevelEditor") ); const FLevelEditorCommands& Commands = LevelEditor.GetLevelEditorCommands(); TSharedRef CommandBindings = LevelEditor.GetGlobalLevelEditorActions(); const FSelectedActorInfo& SelectedActorInfo = DetailBuilder.GetDetailsViewSharedPtr()->GetSelectedActorInfo(); TSharedPtr LevelBox; IDetailCategoryBuilder& ActorCategory = DetailBuilder.EditCategory("Actor", FText::GetEmpty(), ECategoryPriority::Uncommon ); if (GetSelectedActors().Num() == 1) { if (AActor* Actor = GEditor->GetSelectedActors()->GetTop()) { if (Actor->GetActorGuid().IsValid()) { const FText ActorGuidText = FText::FromString(Actor->GetActorGuid().ToString()); ActorCategory.AddCustomRow( LOCTEXT("ActorGuid", "ActorGuid") ) .NameContent() [ SNew(STextBlock) .Text(LOCTEXT("ActorGuid2", "Actor Guid")) .ToolTipText(LOCTEXT("ActorGuid_ToolTip", "Actor Guid")) .Font(IDetailLayoutBuilder::GetDetailFont()) ] .ValueContent() [ SNew(STextBlock) .Text(ActorGuidText) .Font(IDetailLayoutBuilder::GetDetailFont()) .IsEnabled(false) ]; if (Actor->GetActorInstanceGuid() != Actor->GetActorGuid()) { const FText ActorInstanceGuidText = FText::FromString(Actor->GetActorInstanceGuid().ToString()); ActorCategory.AddCustomRow( LOCTEXT("ActorInstanceGuid", "ActorInstanceGuid") ) .NameContent() [ SNew(STextBlock) .Text(LOCTEXT("ActorInstanceGuid2", "Actor Instance Guid")) .ToolTipText(LOCTEXT("ActorInstanceGuid_ToolTip", "Actor Instance Guid")) .Font(IDetailLayoutBuilder::GetDetailFont()) ] .ValueContent() [ SNew(STextBlock) .Text(ActorInstanceGuidText) .Font(IDetailLayoutBuilder::GetDetailFont()) .IsEnabled(false) ]; } } if (Actor->GetContentBundleGuid().IsValid()) { UWorldPartition* WorldPartition = Actor->GetWorld() ? Actor->GetWorld()->GetWorldPartition() : nullptr; if (WorldPartition && WorldPartition->IsContentBundleEnabled()) { const FText ActorContentBundleGuidText = FText::FromString(Actor->GetContentBundleGuid().ToString()); ActorCategory.AddCustomRow( LOCTEXT("ContentBundleGuid", "ContentBundleGuid") ) .NameContent() [ SNew(STextBlock) .Text(LOCTEXT("ContentBundleGuid2", "Content Bundle Guid")) .ToolTipText(LOCTEXT("ActorContentBundleGuid_ToolTip", "Actor Content BundleGuid")) .Font(IDetailLayoutBuilder::GetDetailFont()) ] .ValueContent() [ SNew(STextBlock) .Text(ActorContentBundleGuidText) .Font(IDetailLayoutBuilder::GetDetailFont()) .IsEnabled(false) ]; } } } }; #if 1 // Create the info buttons per level for ( auto LevelIt( ActorsPerLevelCount.CreateConstIterator() ); LevelIt; ++LevelIt) { ULevel* Level = LevelIt.Key(); int32 SelectedActorCountInLevel = LevelIt.Value(); // Get a description of the level FText LevelDescription = FText::FromString( FPackageName::GetShortName( Level->GetOutermost()->GetFName() ) ); if (Level == Level->OwningWorld->PersistentLevel) { LevelDescription = NSLOCTEXT("UnrealEd", "PersistentLevel", "Persistent Level"); } // Create a description and tooltip for the actor count/selection hyperlink const FText ActorCountDescription = FText::Format( LOCTEXT("SelectedActorsInOneLevel", "{0} selected in"), FText::AsNumber( SelectedActorCountInLevel ) ); const FText Tooltip = FText::Format( LOCTEXT("SelectedActorsHyperlinkTooltip", "Narrow the selection set to just the actors in {0}"), LevelDescription); // Create the row for this level TWeakObjectPtr WeakLevelPtr = Level; ActorCategory.AddCustomRow( LOCTEXT("SelectionFilter", "Selected") ) .NameContent() [ SNew(SHyperlink) .Style(FAppStyle::Get(), "HoverOnlyHyperlink") .OnNavigate(this, &FActorDetails::OnNarrowSelectionSetToSpecificLevel, WeakLevelPtr) .Text(ActorCountDescription) .TextStyle(FAppStyle::Get(), "DetailsView.HyperlinkStyle") .ToolTipText(Tooltip) ] .ValueContent() .MaxDesiredWidth(0.f) [ SNew(STextBlock) .Text(LevelDescription) .Font(IDetailLayoutBuilder::GetDetailFont()) ]; } #endif // Convert Actor Menu // WorldSettings should never convert to another class type if( SelectedActorInfo.SelectionClass != AWorldSettings::StaticClass() && SelectedActorInfo.HasConvertableAsset() ) { ActorCategory.AddCustomRow( LOCTEXT("ConvertMenu", "Convert") ) .NameContent() [ SNew(STextBlock) .Text(LOCTEXT("ConvertActor", "Convert Actor")) .ToolTipText(LOCTEXT("ConvertActor_ToolTip", "Convert actors to different types")) .Font(IDetailLayoutBuilder::GetDetailFont()) ] .ValueContent() [ MakeConvertMenu( SelectedActorInfo ) ]; } } const TArray< TWeakObjectPtr >& FActorDetails::GetSelectedActors() const { return SelectedActors; } #undef LOCTEXT_NAMESPACE