// Copyright Epic Games, Inc. All Rights Reserved. #include "SActorDetails.h" #include "Components/ActorComponent.h" #include "Components/SceneComponent.h" #include "DetailsViewObjectFilter.h" #include "Editor.h" #include "Editor/UnrealEdEngine.h" #include "Styling/AppStyle.h" #include "Elements/Framework/EngineElementsLibrary.h" #include "Elements/Framework/TypedElementRegistry.h" #include "Elements/Framework/TypedElementSelectionSet.h" #include "Elements/Interfaces/TypedElementWorldInterface.h" #include "Engine/Blueprint.h" #include "Engine/BlueprintGeneratedClass.h" #include "Engine/Selection.h" #include "GameFramework/Actor.h" #include "HAL/FileManager.h" #include "IDetailRootObjectCustomization.h" #include "IDetailsView.h" #include "Kismet2/ComponentEditorUtils.h" #include "Kismet2/KismetEditorUtilities.h" #include "LevelEditor.h" #include "LevelEditorGenericDetails.h" #include "LevelEditorSubsystem.h" #include "Modules/ModuleManager.h" #include "PropertyEditorModule.h" #include "SSubobjectEditor.h" #include "SSubobjectEditorModule.h" #include "SSubobjectInstanceEditor.h" #include "ScopedTransaction.h" #include "SourceCodeNavigation.h" #include "SubobjectData.h" #include "SubobjectDataSubsystem.h" #include "UnrealEdGlobals.h" #include "Widgets/Docking/SDockTab.h" #include "Widgets/Images/SImage.h" #include "Widgets/Input/SButton.h" #include "Widgets/Layout/SBorder.h" #include "Widgets/Layout/SBox.h" #include "Widgets/Layout/SSplitter.h" #include "Widgets/SBoxPanel.h" #include "Widgets/Text/SRichTextBlock.h" #define LOCTEXT_NAMESPACE "SActorDetails" namespace UE::LevelEditor::Private { class SElementSelectionDetailsButtons : public SCompoundWidget { public: SLATE_BEGIN_ARGS(SElementSelectionDetailsButtons) {} SLATE_END_ARGS() void Construct(const FArguments& InArgs, TSharedRef SubobjectEditorButtonBox, TFunction>()>&& InGetDetailsHandles) { GetDetailsHandles = MoveTemp(InGetDetailsHandles); ChildSlot [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .VAlign(VAlign_Center) .Padding(0.0f, 0.0f, 4.0f, 0.0f) .AutoWidth() [ SAssignNew(PromoteElementButton, SButton) .ButtonStyle(FAppStyle::Get(), "DetailsView.NameAreaButton") .ContentPadding(0.f) .HAlign(HAlign_Center) .VAlign(VAlign_Center) .AddMetaData(FTagMetaData(TEXT("Element.PromoteElement"))) .OnClicked(this, &SElementSelectionDetailsButtons::OnPromoteElement) .ToolTipText(LOCTEXT("PromoteElementTooltip", "Promote the selected elements.")) [ SNew(SImage) .Image(FAppStyle::Get().GetBrush("Icons.PromoteElements")) ] ] + SHorizontalBox::Slot() .VAlign(VAlign_Center) .AutoWidth() [ SubobjectEditorButtonBox ] ]; } void UpdateSelection(TArrayView>* InDetailsElementsPtr) { TArrayView> DetailsElementsView; TArray> DetailsElementsContainer; if (InDetailsElementsPtr) { DetailsElementsView = *InDetailsElementsPtr; } else { DetailsElementsContainer = GetDetailsHandles(); DetailsElementsView = MakeArrayView(DetailsElementsContainer); } bool bCanPromoteAnElement = false; UTypedElementRegistry* Registry = UTypedElementRegistry::GetInstance(); for (const TTypedElement& DetailsElement : DetailsElementsView) { if (TTypedElement WorldElement = Registry->GetElement(DetailsElement)) { if (WorldElement.CanPromoteElement()) { bCanPromoteAnElement = true; break; } } } if (bCanPromoteAnElement) { PromoteElementButton->SetVisibility(EVisibility::Visible); } else { PromoteElementButton->SetVisibility(EVisibility::Collapsed); } } private: FReply OnPromoteElement() { FScopedTransaction Transaction(LOCTEXT("PromoteElementsTransaction", "Promote Elements")); TArray> DetailsElements = GetDetailsHandles(); TArray Actors; Actors.Reserve(DetailsElements.Num()); UTypedElementSelectionSet* SelectionSet = GEditor->GetEditorSubsystem()->GetSelectionSet(); FTypedElementSelectionOptions SelectionOptions; UTypedElementRegistry* Registry = UTypedElementRegistry::GetInstance(); for (const TTypedElement& DetailsElement : DetailsElements) { if (TTypedElement WorldElement = Registry->GetElement(DetailsElement)) { if (WorldElement.CanPromoteElement()) { SelectionSet->DeselectElement(WorldElement, SelectionOptions); } if (FTypedElementHandle PromotedElement = WorldElement.PromoteElement()) { SelectionSet->SelectElement(PromotedElement, SelectionOptions); } } } return FReply::Handled(); } TSharedPtr PromoteElementButton; TFunction>()> GetDetailsHandles; }; } class SActorDetailsUneditableComponentWarning : public SCompoundWidget { public: SLATE_BEGIN_ARGS(SActorDetailsUneditableComponentWarning) : _WarningText() , _OnHyperlinkClicked() {} /** The rich text to show in the warning */ SLATE_ATTRIBUTE(FText, WarningText) /** Called when the hyperlink in the rich text is clicked */ SLATE_EVENT(FSlateHyperlinkRun::FOnClick, OnHyperlinkClicked) SLATE_END_ARGS() /** Constructs the widget */ void Construct(const FArguments& InArgs) { ChildSlot [ SNew(SBorder) .BorderImage(FAppStyle::Get().GetBrush("Brushes.Panel")) [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .AutoWidth() .HAlign(HAlign_Center) .VAlign(VAlign_Center) .Padding(2.f) [ SNew(SImage) .Image(FAppStyle::Get().GetBrush("Icons.Warning")) ] + SHorizontalBox::Slot() .VAlign(VAlign_Center) .Padding(2.f) [ SNew(SRichTextBlock) .DecoratorStyleSet(&FAppStyle::Get()) .Justification(ETextJustify::Left) .TextStyle(FAppStyle::Get(), "DetailsView.BPMessageTextStyle") .Text(InArgs._WarningText) .AutoWrapText(true) + SRichTextBlock::HyperlinkDecorator(TEXT("HyperlinkDecorator"), InArgs._OnHyperlinkClicked) ] ] ]; } }; void SActorDetails::Construct(const FArguments& InArgs, UTypedElementSelectionSet* InSelectionSet, const FName TabIdentifier, TSharedPtr InCommandList, TSharedPtr InTabManager) { SelectionSet = InSelectionSet; checkf(SelectionSet, TEXT("SActorDetails must be constructed with a valid selection set!")); FCoreUObjectDelegates::OnObjectsReplaced.AddRaw(this, &SActorDetails::OnObjectsReplaced); FCoreUObjectDelegates::OnObjectPropertyChanged.AddRaw(this, &SActorDetails::OnObjectPropertyChanged); FLevelEditorModule& LevelEditor = FModuleManager::GetModuleChecked("LevelEditor"); LevelEditor.OnComponentsEdited().AddRaw(this, &SActorDetails::OnComponentsEditedInWorld); FDetailsViewArgs DetailsViewArgs; DetailsViewArgs.bUpdatesFromSelection = true; DetailsViewArgs.bLockable = true; DetailsViewArgs.bAllowFavoriteSystem = true; DetailsViewArgs.NameAreaSettings = FDetailsViewArgs::ObjectsUseNameArea | FDetailsViewArgs::ComponentsAndActorsUseNameArea; DetailsViewArgs.NotifyHook = GUnrealEd; DetailsViewArgs.ViewIdentifier = TabIdentifier; DetailsViewArgs.bCustomNameAreaLocation = true; DetailsViewArgs.bCustomFilterAreaLocation = true; DetailsViewArgs.DefaultsOnlyVisibility = EEditDefaultsOnlyNodeVisibility::Hide; DetailsViewArgs.HostCommandList = InCommandList; DetailsViewArgs.HostTabManager = InTabManager; DetailsViewArgs.bShowSectionSelector = true; FPropertyEditorModule& PropPlugin = FModuleManager::LoadModuleChecked("PropertyEditor"); DetailsView = PropPlugin.CreateDetailView(DetailsViewArgs); auto IsPropertyVisible = [](const FPropertyAndParent& PropertyAndParent) { // For details views in the level editor all properties are the instanced versions if(PropertyAndParent.Property.HasAllPropertyFlags(CPF_DisableEditOnInstance)) { return false; } return true; }; DetailsView->SetIsPropertyVisibleDelegate(FIsPropertyVisible::CreateLambda(IsPropertyVisible)); DetailsView->SetIsPropertyReadOnlyDelegate(FIsPropertyReadOnly::CreateSP(this, &SActorDetails::IsPropertyReadOnly)); DetailsView->SetIsPropertyEditingEnabledDelegate(FIsPropertyEditingEnabled::CreateSP(this, &SActorDetails::IsPropertyEditingEnabled)); // Set up a delegate to call to add generic details to the view DetailsView->SetGenericLayoutDetailsDelegate(FOnGetDetailCustomizationInstance::CreateStatic(&FLevelEditorGenericDetails::MakeInstance)); GEditor->RegisterForUndo(this); ComponentsBox = SNew(SBox) .Padding(FMargin(2.0f, 0.0f, 2.0f, 0.0f)) .Visibility(this, &SActorDetails::GetComponentEditorVisibility); FModuleManager::LoadModuleChecked("SubobjectEditor"); SubobjectEditor = SNew(SSubobjectInstanceEditor) .ObjectContext(this, &SActorDetails::GetActorContextAsObject) .AllowEditing(this, &SActorDetails::GetAllowComponentTreeEditing) .OnSelectionUpdated(this, &SActorDetails::OnSubobjectEditorTreeViewSelectionChanged) .OnItemDoubleClicked(this, &SActorDetails::OnSubobjectEditorTreeViewItemDoubleClicked); ComponentsBox->SetContent(SubobjectEditor.ToSharedRef()); TSharedRef SubobjectEditorButtonBox = SubobjectEditor->GetToolButtonsBox().ToSharedRef(); SubobjectEditorButtonBox->SetVisibility(MakeAttributeSP(this, &SActorDetails::GetComponentEditorButtonsVisibility)); TFunction>()> GetDetailsHandles = [this]() { TArray> DetailsElements; // Regenerate the details handles if (bHasSelectionOverride) { UTypedElementRegistry* Registry = UTypedElementRegistry::GetInstance(); DetailsElements.Reserve(SelectionOverrideActors.Num()); for (AActor* Actor : SelectionOverrideActors) { if (FTypedElementHandle ActorElementHandle = UEngineElementsLibrary::AcquireEditorActorElementHandle(Actor)) { if (TTypedElement ActorDetailsHandle = Registry->GetElement(ActorElementHandle)) { // Check if the actor element does to provide a details object if (TUniquePtr ElementDetailsObject = ActorDetailsHandle.GetDetailsObject()) { DetailsElements.Add(ActorDetailsHandle); } } } } } else { DetailsElements.Reserve(SelectionSet->GetNumSelectedElements()); SelectionSet->ForEachSelectedElement([&DetailsElements](const TTypedElement& InDetailsElement) { // Check if the element does to provide a details object if (TUniquePtr ElementDetailsObject = InDetailsElement.GetDetailsObject()) { DetailsElements.Add(InDetailsElement); } return true; }); } return DetailsElements; }; TSharedRef ButtonBox = SAssignNew(ElementSelectionDetailsButtons, UE::LevelEditor::Private::SElementSelectionDetailsButtons, SubobjectEditorButtonBox, MoveTemp(GetDetailsHandles)); DetailsView->SetNameAreaCustomContent(ButtonBox); ChildSlot [ SNew(SVerticalBox) +SVerticalBox::Slot() .Padding(10.f, 4.f, 0.f, 0.f) .AutoHeight() [ DetailsView->GetNameAreaWidget().ToSharedRef() ] +SVerticalBox::Slot() [ SAssignNew(DetailsSplitter, SSplitter) .MinimumSlotHeight(80.0f) .Orientation(Orient_Vertical) .Style(FAppStyle::Get(), "SplitterDark") .PhysicalSplitterHandleSize(2.0f) + SSplitter::Slot() [ SNew( SVerticalBox ) +SVerticalBox::Slot() .AutoHeight() .Padding( FMargin(0.f,0.f,0.f,1.f) ) [ SNew(SActorDetailsUneditableComponentWarning) .Visibility(this, &SActorDetails::GetUCSComponentWarningVisibility) .WarningText(NSLOCTEXT("SActorDetails", "BlueprintUCSComponentWarning", "Components created by the User Construction Script can only be edited in the Blueprint")) .OnHyperlinkClicked(this, &SActorDetails::OnBlueprintedComponentWarningHyperlinkClicked) ] +SVerticalBox::Slot() .AutoHeight() .Padding( FMargin(0.f,0.f,0.f,1.f) ) [ SNew(SActorDetailsUneditableComponentWarning) .Visibility(this, &SActorDetails::GetInheritedBlueprintComponentWarningVisibility) .WarningText(NSLOCTEXT("SActorDetails", "BlueprintUneditableInheritedComponentWarning", "Components flagged as not editable when inherited must be edited in the Blueprint")) .OnHyperlinkClicked(this, &SActorDetails::OnBlueprintedComponentWarningHyperlinkClicked) ] +SVerticalBox::Slot() .AutoHeight() .Padding( FMargin(0.f,0.f,0.f,1.f) ) [ SNew(SActorDetailsUneditableComponentWarning) .Visibility(this, &SActorDetails::GetNativeComponentWarningVisibility) .WarningText(NSLOCTEXT("SActorDetails", "UneditableNativeComponentWarning", "Native components are editable when declared as a FProperty in C++")) .OnHyperlinkClicked(this, &SActorDetails::OnNativeComponentWarningHyperlinkClicked) ] + SVerticalBox::Slot() .AutoHeight() [ DetailsView->GetFilterAreaWidget().ToSharedRef() ] + SVerticalBox::Slot() [ DetailsView.ToSharedRef() ] ] ] ]; DetailsSplitter->AddSlot(0) .Value(.2f) [ ComponentsBox.ToSharedRef() ]; // Immediately update (otherwise we will appear empty) RefreshSelection(/*bForceRefresh*/true); } SActorDetails::~SActorDetails() { if (GEditor) { GEditor->UnregisterForUndo(this); } FCoreUObjectDelegates::OnObjectsReplaced.RemoveAll(this); FCoreUObjectDelegates::OnObjectPropertyChanged.RemoveAll(this); RemoveBPComponentCompileEventDelegate(); FLevelEditorModule* LevelEditor = FModuleManager::GetModulePtr("LevelEditor"); if (LevelEditor != nullptr) { LevelEditor->OnComponentsEdited().RemoveAll(this); } } bool SActorDetails::IsObservingSelectionSet(const UTypedElementSelectionSet* InSelectionSet) const { return SelectionSet == InSelectionSet; } void SActorDetails::RefreshSelection(const bool bForceRefresh) { if (bSelectionGuard) { return; } TArray> DetailsElements; DetailsElements.Reserve(SelectionSet->GetNumSelectedElements()); SelectionSet->ForEachSelectedElement([&DetailsElements](const TTypedElement& InDetailsElement) { DetailsElements.Add(InDetailsElement); return true; }); bHasSelectionOverride = false; SelectionOverrideActors.Reset(); RefreshTopLevelElements(DetailsElements, bForceRefresh, /*bOverrideLock*/false); } void SActorDetails::OverrideSelection(const TArray& InActors, const bool bForceRefresh) { UTypedElementRegistry* Registry = UTypedElementRegistry::GetInstance(); TArray> DetailsElements; DetailsElements.Reserve(InActors.Num()); for (AActor* Actor : InActors) { if (FTypedElementHandle ActorElementHandle = UEngineElementsLibrary::AcquireEditorActorElementHandle(Actor)) { if (TTypedElement ActorDetailsHandle = Registry->GetElement(ActorElementHandle)) { DetailsElements.Add(MoveTemp(ActorDetailsHandle)); } } } bHasSelectionOverride = true; SelectionOverrideActors = InActors; RefreshTopLevelElements(DetailsElements, bForceRefresh, /*bOverrideLock*/false); } void SActorDetails::RefreshTopLevelElements(TArrayView> InDetailsElements, const bool bForceRefresh, const bool bOverrideLock) { // Nothing to do if this view is locked! if (DetailsView->IsLocked() && !bOverrideLock) { return; } // Build the array of top-level elements to edit TopLevelElements.Reset(InDetailsElements.Num()); for (const TTypedElement& DetailsElement : InDetailsElements) { if (DetailsElement.IsTopLevelElement()) { if (TUniquePtr ElementDetailsObject = DetailsElement.GetDetailsObject()) { TopLevelElements.Add(MoveTemp(ElementDetailsObject)); } } } // Update the underlying details view and the Elements buttons SetElementDetailsObjects(TopLevelElements, bForceRefresh, bOverrideLock, &InDetailsElements); // Update the Subobject tree { // Enable the selection guard to prevent OnTreeSelectionChanged() from altering the editor's component selection TGuardValue SelectionGuard(bSelectionGuard, true); SubobjectEditor->UpdateTree(); UpdateComponentTreeFromEditorSelection(); } // Draw attention to this tab if needed if (TSharedPtr TabManager = DetailsView->GetHostTabManager()) { TSharedPtr Tab = TabManager->FindExistingLiveTab(DetailsView->GetIdentifier()); if (Tab.IsValid() && !Tab->IsForeground()) { Tab->FlashTab(); } } } void SActorDetails::RefreshSubobjectTreeElements(TArrayView InSelectedNodes, const bool bForceRefresh, const bool bOverrideLock) { // Nothing to do if this view is locked! if (DetailsView->IsLocked() && !bOverrideLock) { return; } // Does the Subobject tree have components selected? TArray Components; if (const AActor* Actor = GetActorContext()) { for (const FSubobjectEditorTreeNodePtrType& SelectedNode : InSelectedNodes) { if (SelectedNode) { if(const FSubobjectData* Data = SelectedNode->GetDataSource()) { if(Data->IsRootActor()) { // If the actor node is selected then we ignore the component selection Components.Reset(); break; } if (Data->IsComponent()) { if (const UActorComponent* Component = Data->FindComponentInstanceInActor(Actor)) { Components.Add(Component); } } } } } } SubobjectTreeElements.Reset(Components.Num()); if (Components.Num() > 0) { UTypedElementRegistry* Registry = UTypedElementRegistry::GetInstance(); for (const UActorComponent* Component : Components) { if (FTypedElementHandle ComponentElementHandle = UEngineElementsLibrary::AcquireEditorComponentElementHandle(Component)) { if (TTypedElement ComponentDetailsHandle = Registry->GetElement(ComponentElementHandle)) { if (TUniquePtr ElementDetailsObject = ComponentDetailsHandle.GetDetailsObject()) { SubobjectTreeElements.Add(MoveTemp(ElementDetailsObject)); } } } } // Use the component elements SetElementDetailsObjects(SubobjectTreeElements, bForceRefresh, bOverrideLock); } else { // Use the top-level elements SetElementDetailsObjects(TopLevelElements, bForceRefresh, bOverrideLock); } } void SActorDetails::SetElementDetailsObjects(TArrayView> InElementDetailsObjects, const bool bForceRefresh, const bool bOverrideLock, TArrayView>* InDetailsElementsPtr) { TArray DetailsObjects; DetailsObjects.Reserve(InElementDetailsObjects.Num()); for (const TUniquePtr& ElementDetailsObject : InElementDetailsObjects) { if (UObject* DetailsObject = ElementDetailsObject->GetObject()) { DetailsObjects.Add(DetailsObject); } } DetailsView->SetObjects(DetailsObjects, bForceRefresh, bOverrideLock); ElementSelectionDetailsButtons->UpdateSelection(InDetailsElementsPtr); } void SActorDetails::PostUndo(bool bSuccess) { // Enable the selection guard to prevent OnTreeSelectionChanged() from altering the editor's component selection TGuardValue SelectionGuard(bSelectionGuard, true); // Refresh the tree and update the selection to match the world SubobjectEditor->UpdateTree(); UpdateComponentTreeFromEditorSelection(); } void SActorDetails::PostRedo(bool bSuccess) { PostUndo(bSuccess); } void SActorDetails::AddReferencedObjects(FReferenceCollector& Collector) { for (const TUniquePtr& TopLevelElement : TopLevelElements) { TopLevelElement->AddReferencedObjects(Collector); } } FString SActorDetails::GetReferencerName() const { return TEXT("SActorDetails"); } void SActorDetails::SetActorDetailsRootCustomization(TSharedPtr InActorDetailsObjectFilter, TSharedPtr ActorDetailsRootCustomization) { if (InActorDetailsObjectFilter.IsValid()) { DisplayManager = InActorDetailsObjectFilter->GetDisplayManager(); } DetailsView->SetObjectFilter(InActorDetailsObjectFilter); DetailsView->SetRootObjectCustomizationInstance(ActorDetailsRootCustomization); DetailsView->ForceRefresh(); } void SActorDetails::SetSubobjectEditorUICustomization(TSharedPtr ActorDetailsSubobjectEditorUICustomization) { if(SubobjectEditor.IsValid()) { SubobjectEditor->SetUICustomization(ActorDetailsSubobjectEditorUICustomization); } } void SActorDetails::OnComponentsEditedInWorld() { if (AActor* Actor = GetActorContext()) { if (SelectionSet->IsElementSelected(UEngineElementsLibrary::AcquireEditorActorElementHandle(Actor), FTypedElementIsSelectedOptions())) { // The component composition of the observed actor has changed, so rebuild the node tree TGuardValue SelectionGuard(bSelectionGuard, true); // Refresh the tree and update the selection to match the world SubobjectEditor->UpdateTree(); DetailsView->ForceRefresh(); } } } bool SActorDetails::GetAllowComponentTreeEditing() const { return GEditor->PlayWorld == nullptr; } AActor* SActorDetails::GetActorContext() const { return TopLevelElements.Num() == 1 ? Cast(TopLevelElements[0]->GetObject()) : nullptr; } UObject* SActorDetails::GetActorContextAsObject() const { return GetActorContext(); } void SActorDetails::OnSubobjectEditorTreeViewSelectionChanged(const TArray& SelectedNodes) { if (bSelectionGuard) { // Preventing selection changes from having an effect... return; } if (SelectedNodes.Num() == 0) { // Don't respond to de-selecting everything... return; } AActor* ActorContext = GetActorContext(); if (!ActorContext) { // The Subobject editor requires an actor context... return; } if (SelectedNodes.Num() > 1 && SelectedBPComponentBlueprint.IsValid()) { // Remove the compilation delegate if we are no longer displaying the full details for a single blueprint component. RemoveBPComponentCompileEventDelegate(); } else if (SelectedNodes.Num() == 1 && SelectedNodes[0]->IsComponentNode()) { // Add delegate to monitor blueprint component compilation if we have a full details view ( i.e. single selection ) FSubobjectData* SelectedData = SelectedNodes[0]->GetDataSource(); if (UActorComponent* Component = const_cast(SelectedData->FindComponentInstanceInActor(ActorContext))) { if (UBlueprintGeneratedClass* ComponentBPGC = Cast(Component->GetClass())) { if (UBlueprint* ComponentBlueprint = Cast(ComponentBPGC->ClassGeneratedBy)) { AddBPComponentCompileEventDelegate(ComponentBlueprint); } } } } // We only actually update the editor selection state if we're not locked if (!DetailsView->IsLocked()) { TArray NewEditorSelection; NewEditorSelection.Add(UEngineElementsLibrary::AcquireEditorActorElementHandle(ActorContext)); for (const FSubobjectEditorTreeNodePtrType& SelectedNode : SelectedNodes) { if (SelectedNode) { if(FSubobjectData* Data = SelectedNode->GetDataSource()) { if(Data->IsRootActor()) { // If the actor node is selected then we ignore the component selection NewEditorSelection.Reset(); NewEditorSelection.Add(UEngineElementsLibrary::AcquireEditorActorElementHandle(ActorContext)); break; } if (Data->IsComponent()) { if (const UActorComponent* Component = Data->FindComponentInstanceInActor(ActorContext)) { NewEditorSelection.Add(UEngineElementsLibrary::AcquireEditorComponentElementHandle(Component)); } } } } } // Note: this transaction should not take place if we are in the middle of executing an undo or redo because it would clear the top of the transaction stack. const bool bShouldActuallyTransact = !GIsTransacting; const FScopedTransaction Transaction(NSLOCTEXT("UnrealEd", "ClickingOnComponentInTree", "Clicking on Component (tree view)"), bShouldActuallyTransact); // Enable the selection guard to prevent OnEditorSelectionChanged() from altering the contents of the SubobjectTreeWidget TGuardValue SelectionGuard(bSelectionGuard, true); SelectionSet->SetSelection(NewEditorSelection, FTypedElementSelectionOptions()); SelectionSet->NotifyPendingChanges(); // Fire while still under the selection guard } // Update the underlying details view RefreshSubobjectTreeElements(SelectedNodes, /*bForceRefresh*/false, DetailsView->IsLocked()); } void SActorDetails::OnSubobjectEditorTreeViewItemDoubleClicked(const FSubobjectEditorTreeNodePtrType ClickedNode) { if (ClickedNode && ClickedNode->IsComponentNode()) { if (const USceneComponent* SceneComponent = Cast(ClickedNode->GetComponentTemplate())) { const bool bActiveViewportOnly = false; GEditor->MoveViewportCamerasToComponent(SceneComponent, bActiveViewportOnly); } } } void SActorDetails::UpdateComponentTreeFromEditorSelection() { if (DetailsView->IsLocked()) { return; } // Enable the selection guard to prevent OnTreeSelectionChanged() from altering the editor's component selection TGuardValue SelectionGuard(bSelectionGuard, true); TSharedPtr TreeWidget = SubobjectEditor->GetDragDropTree(); // Update the tree selection to match the level editor component selection SubobjectEditor->ClearSelection(); SelectionSet->ForEachSelectedObject([this, &TreeWidget](UActorComponent* InComponent) { FSubobjectEditorTreeNodePtrType TreeNode = SubobjectEditor->FindSlateNodeForObject(InComponent, false); if (TreeNode && TreeNode->GetComponentTemplate()) { TreeWidget->RequestScrollIntoView(TreeNode); TreeWidget->SetItemSelection(TreeNode, true); check(InComponent == TreeNode->GetComponentTemplate() || InComponent->GetArchetype() == TreeNode->GetComponentTemplate()); } return true; }); TArray SelectedNodes = SubobjectEditor->GetSelectedNodes(); if (SelectedNodes.Num() == 0) { SubobjectEditor->SelectRoot(); SelectedNodes = SubobjectEditor->GetSelectedNodes(); } // Update the underlying details view RefreshSubobjectTreeElements(SelectedNodes, bSelectedComponentRecompiled, /*bOverrideLock*/false); } bool SActorDetails::IsPropertyReadOnly(const FPropertyAndParent& PropertyAndParent) const { bool bIsReadOnly = false; for (const FSubobjectEditorTreeNodePtrType& Node : SubobjectEditor->GetSelectedNodes()) { const UActorComponent* Component = Node->GetComponentTemplate(); if (Component && Component->CreationMethod == EComponentCreationMethod::SimpleConstructionScript) { TSet UCSModifiedProperties; Component->GetUCSModifiedProperties(UCSModifiedProperties); if (UCSModifiedProperties.Contains(&PropertyAndParent.Property) || (PropertyAndParent.ParentProperties.Num() > 0 && UCSModifiedProperties.Contains(PropertyAndParent.ParentProperties[0]))) { bIsReadOnly = true; break; } } } return bIsReadOnly; } bool SActorDetails::IsPropertyEditingEnabled() const { FLevelEditorModule& LevelEditor = FModuleManager::GetModuleChecked("LevelEditor"); if (!LevelEditor.AreObjectsEditable(DetailsView->GetSelectedObjects())) { return false; } bool bIsEditable = true; for (const FSubobjectEditorTreeNodePtrType& Node : SubobjectEditor->GetSelectedNodes()) { if(const FSubobjectData* Data = Node->GetDataSource()) { bIsEditable = Data->CanEdit(); if (!bIsEditable) { break; } } } return bIsEditable; } void SActorDetails::OnBlueprintedComponentWarningHyperlinkClicked(const FSlateHyperlinkRun::FMetadata& Metadata) { UBlueprint* Blueprint = SubobjectEditor->GetBlueprint(); if (Blueprint) { // Open the blueprint GEditor->EditObject(Blueprint); } } void SActorDetails::OnNativeComponentWarningHyperlinkClicked(const FSlateHyperlinkRun::FMetadata& Metadata) { // Find the closest native parent UBlueprint* Blueprint = SubobjectEditor->GetBlueprint(); UClass* ParentClass = Blueprint ? *Blueprint->ParentClass : GetActorContext()->GetClass(); while (ParentClass && !ParentClass->HasAllClassFlags(CLASS_Native)) { ParentClass = ParentClass->GetSuperClass(); } if (ParentClass) { FString NativeParentClassHeaderPath; const bool bFileFound = FSourceCodeNavigation::FindClassHeaderPath(ParentClass, NativeParentClassHeaderPath) && ( IFileManager::Get().FileSize(*NativeParentClassHeaderPath) != INDEX_NONE ); if (bFileFound) { const FString AbsoluteHeaderPath = IFileManager::Get().ConvertToAbsolutePathForExternalAppForRead(*NativeParentClassHeaderPath); FSourceCodeNavigation::OpenSourceFile(AbsoluteHeaderPath); } } } EVisibility SActorDetails::GetComponentEditorVisibility() const { // see if we need to hide the editor due to the current object display const bool bHideEditorFromDetailsView = (DisplayManager.IsValid() && DisplayManager->ShouldHideComponentEditor() ); return GetActorContext() && !bHideEditorFromDetailsView ? EVisibility::Visible : EVisibility::Collapsed; } EVisibility SActorDetails::GetComponentEditorButtonsVisibility() const { return GetActorContext() ? EVisibility::Visible : EVisibility::Collapsed; } EVisibility SActorDetails::GetUCSComponentWarningVisibility() const { bool bIsUneditableBlueprintComponent = false; // Check to see if any selected components are inherited from blueprint for (const FSubobjectEditorTreeNodePtrType& Node : SubobjectEditor->GetSelectedNodes()) { if(const FSubobjectData* Data = Node->GetDataSource()) { if (!Data->IsNativeComponent()) { const UActorComponent* Component = Data->GetComponentTemplate(); bIsUneditableBlueprintComponent = Component ? Component->CreationMethod == EComponentCreationMethod::UserConstructionScript : false; if (bIsUneditableBlueprintComponent) { break; } } } } return bIsUneditableBlueprintComponent ? EVisibility::Visible : EVisibility::Collapsed; } bool NotEditableSetByBlueprint(const UActorComponent* Component) { // Determine if it is locked out from a blueprint or from the native UActorComponent* Archetype = CastChecked(Component->GetArchetype()); while (Archetype) { if (Archetype->GetOuter()->IsA() || Archetype->GetOuter()->GetClass()->HasAllClassFlags(CLASS_CompiledFromBlueprint)) { if (!Archetype->bEditableWhenInherited) { return true; } Archetype = CastChecked(Archetype->GetArchetype()); } else { Archetype = nullptr; } } return false; } EVisibility SActorDetails::GetInheritedBlueprintComponentWarningVisibility() const { bool bIsUneditableBlueprintComponent = false; // Check to see if any selected components are inherited from blueprint for (const FSubobjectEditorTreeNodePtrType& Node : SubobjectEditor->GetSelectedNodes()) { if(const FSubobjectData* Data = Node->GetDataSource()) { if (!Data->IsNativeComponent()) { if (const UActorComponent* Component = Data->GetComponentTemplate()) { if (!Component->IsEditableWhenInherited() && Component->CreationMethod == EComponentCreationMethod::SimpleConstructionScript) { bIsUneditableBlueprintComponent = true; break; } } } else if (!Data->CanEdit() && NotEditableSetByBlueprint(Data->GetComponentTemplate())) { bIsUneditableBlueprintComponent = true; break; } } } return bIsUneditableBlueprintComponent ? EVisibility::Visible : EVisibility::Collapsed; } EVisibility SActorDetails::GetNativeComponentWarningVisibility() const { bool bIsUneditableNative = false; for (const FSubobjectEditorTreeNodePtrType& Node : SubobjectEditor->GetSelectedNodes()) { if(const FSubobjectData* Data = Node->GetDataSource()) { // Check to see if the component is native and not editable if (Data->IsNativeComponent() && !Data->CanEdit() && !NotEditableSetByBlueprint(Data->GetComponentTemplate())) { bIsUneditableNative = true; break; } } } return bIsUneditableNative ? EVisibility::Visible : EVisibility::Collapsed; } void SActorDetails::AddBPComponentCompileEventDelegate(UBlueprint* ComponentBlueprint) { if(SelectedBPComponentBlueprint.Get() != ComponentBlueprint) { RemoveBPComponentCompileEventDelegate(); SelectedBPComponentBlueprint = ComponentBlueprint; // Add blueprint component compilation event delegate if(!ComponentBlueprint->OnCompiled().IsBoundToObject(this)) { ComponentBlueprint->OnCompiled().AddSP(this, &SActorDetails::OnBlueprintComponentCompiled); } } } void SActorDetails::RemoveBPComponentCompileEventDelegate() { // Remove blueprint component compilation event delegate if(SelectedBPComponentBlueprint.IsValid()) { SelectedBPComponentBlueprint.Get()->OnCompiled().RemoveAll(this); SelectedBPComponentBlueprint.Reset(); bSelectedComponentRecompiled = false; } } void SActorDetails::OnBlueprintComponentCompiled(UBlueprint* ComponentBlueprint) { TGuardValue SelectedComponentRecompiledGuard(bSelectedComponentRecompiled, true); UpdateComponentTreeFromEditorSelection(); } void SActorDetails::OnObjectsReplaced(const TMap& InReplacementObjects) { if (bHasSelectionOverride && SelectionOverrideActors.Num() > 0) { bool bHasChanges = false; for (auto It = SelectionOverrideActors.CreateIterator(); It; ++It) { AActor*& Actor = *It; if (UObject* const* ReplacementObjectPtr = InReplacementObjects.Find(Actor)) { bHasChanges = true; AActor* ReplacementActor = Cast(*ReplacementObjectPtr); if (ReplacementActor) { Actor = ReplacementActor; } else { It.RemoveCurrent(); } } } if (bHasChanges) { TArray NewSelection = SelectionOverrideActors; OverrideSelection(NewSelection); } } else { // Enable the selection guard to prevent OnTreeSelectionChanged() from altering the editor's component selection TGuardValue SelectionGuard(bSelectionGuard, true); SubobjectEditor->UpdateTree(); } } void SActorDetails::OnObjectPropertyChanged(UObject* ObjectBeingModified, FPropertyChangedEvent& PropertyChangedEvent) { // Listen for archetype changes to properties that are editable on the instance if (!ObjectBeingModified || !ObjectBeingModified->HasAnyFlags(RF_ClassDefaultObject | RF_ArchetypeObject) || !PropertyChangedEvent.Property || !PropertyChangedEvent.Property->HasAnyPropertyFlags(CPF_Edit) || PropertyChangedEvent.Property->HasAnyPropertyFlags(CPF_DisableEditOnInstance)) { return; } // If the object that changes matches the archetype of an instance that's selected to the property view, // invalidate the cached state so that things like the "reset to default" icon is synced to the archetype. for (const TWeakObjectPtr<>& SelectedObject : DetailsView->GetSelectedObjects()) { if (SelectedObject.IsValid() && SelectedObject->GetArchetype() == ObjectBeingModified) { DetailsView->InvalidateCachedState(); break; } } } #undef LOCTEXT_NAMESPACE