// Copyright Epic Games, Inc. All Rights Reserved. #include "Dialogs/DlgReferenceTree.h" #include "UObject/UObjectIterator.h" #include "Widgets/SWindow.h" #include "Framework/Application/SlateApplication.h" #include "Textures/SlateIcon.h" #include "Framework/Commands/UIAction.h" #include "Framework/Commands/UICommandList.h" #include "Framework/MultiBox/MultiBoxBuilder.h" #include "Widgets/Input/SButton.h" #include "EditorStyleSet.h" #include "Editor/UnrealEdEngine.h" #include "Editor.h" #include "UnrealEdGlobals.h" #include "ObjectTools.h" #include "Subsystems/AssetEditorSubsystem.h" FArchiveGenerateReferenceGraph::FArchiveGenerateReferenceGraph(FReferenceGraph& OutGraph) : CurrentObject(NULL), ObjectGraph(OutGraph) { ArIsObjectReferenceCollector = true; ArIgnoreOuterRef = true; // Iterate over each object.. for (FThreadSafeObjectIterator It; It; ++It) { UObject* Object = *It; // Skip transient and those about to be deleted if (!Object->HasAnyFlags(RF_Transient) && !Object->IsPendingKill()) { // only serialize non actors objects which have not been visited. // actors are skipped because we have don't need them to show the reference tree // @todo, may need to serialize them later for full reference graph. if (!VisitedObjects.Find(Object) && !Object->IsA(AActor::StaticClass())) { // Set the current object to the one we are about to serialize CurrentObject = Object; // This object has been visited. Any serializations after this should skip this object VisitedObjects.Add(Object); Object->Serialize(*this); } } } } FArchive& FArchiveGenerateReferenceGraph::operator<<(UObject*& Object) { // Only look at objects which are valid const bool bValidObject = Object && // Object should not be NULL !Object->HasAnyFlags(RF_Transient) && // Should not be transient !Object->IsPendingKill() && // nor pending kill (Cast(Object) == NULL); // skip UClasses if (bValidObject) { // Determine if a node for the referenced object has already been created FReferenceGraphNode* ReferencedNode = ObjectGraph.FindRef(Object); if (ReferencedNode == NULL) { // If no node has been created, create one now ReferencedNode = ObjectGraph.Add(Object, new FReferenceGraphNode(Object)); } // Find a node for the referencer object. CurrentObject references Object FReferenceGraphNode* ReferencerNode = ObjectGraph.FindRef(CurrentObject); if (ReferencerNode == NULL) { // If node node has been created, create one now ReferencerNode = ObjectGraph.Add(CurrentObject, new FReferenceGraphNode(CurrentObject)); } // Ignore self referencing objects if (Object != CurrentObject) { // Add a new link from the node to what references it. // Links represent references to the object contained in ReferencedNode ReferencedNode->Links.Add(ReferencerNode); } if (!VisitedObjects.Find(Object) && !Object->IsA(AActor::StaticClass())) { // If this object hasnt been visited and is not an actor, serialize it // Store the current object for when we return from serialization UObject* PrevObject = CurrentObject; // Set the new current object CurrentObject = Object; // This object has now been visited VisitedObjects.Add(Object); // Serialize Object->Serialize(*this); // Restore the current object CurrentObject = PrevObject; } } return *this; } namespace ReferenceTreeView { namespace Helpers { /** Shows the passed in object in the content browser (if browsable) or the level (if actor) */ void SelectObjectInEditor(UObject* InObjectToSelect) { AActor* Actor = Cast(InObjectToSelect); if (Actor) { // Do not attempt to select script based objects if (!Actor->HasAnyFlags(RF_ClassDefaultObject)) { // Select and focus in on the actor GEditor->SelectNone(false, true); GEditor->SelectActor(Actor, true, true, true); GEditor->MoveViewportCamerasToActor(*Actor, true); } } else { // Show the object in the content browser. TArray ObjectsToSync; ObjectsToSync.Add(InObjectToSelect); GEditor->SyncBrowserToObjects(ObjectsToSync); } } } // namespace Helpers } // namespace ReferenceTreeView static const FName ColumnID_ReferenceLabel("Reference"); struct FReferenceTreeDataContainer { UObject* Object; TArray ChildrenReferences; FReferenceTreeDataContainer(UObject* InObject): Object(InObject) {} }; /** Widget that represents a row in the sound wave compression's tree control. Generates widgets for each column on demand. */ class SReferenceTreeRow : public STableRow { public: SLATE_BEGIN_ARGS(SReferenceTreeRow) {} SLATE_ARGUMENT(FReferenceTreeItemPtr, Item) SLATE_END_ARGS() /** * Construct the widget * * @param InArgs A declaration from which to construct the widget */ void Construct(const FArguments& InArgs, const TSharedRef& InOwnerTableView) { Item = InArgs._Item; FFormatNamedArguments Args; Args.Add(TEXT("ClassName"), FText::FromString(Item->Object->GetClass()->GetName())); Args.Add(TEXT("ObjectName"), FText::FromString(Item->Object->GetName())); this->ChildSlot [SNew(SHorizontalBox) + SHorizontalBox::Slot() .AutoWidth() [SNew(SExpanderArrow, SharedThis(this))] + SHorizontalBox::Slot() .AutoWidth() [SNew(STextBlock) .Text(FText::Format(NSLOCTEXT("ReferenceTree", "ReferenceTree_Object/ClassTitle", "{ClassName}({ObjectName})"), Args))]]; STableRow::ConstructInternal( STableRow::FArguments() .ShowSelection(true), InOwnerTableView); } private: /** Called when a tree item is double clicked. */ virtual FReply OnMouseButtonDoubleClick(const FGeometry& InMyGeometry, const FPointerEvent& InMouseEvent) override { // Show the object in the editor. I.E show the object in the level if its an actor, or content browser otherwise. if (Item->Object) { ReferenceTreeView::Helpers::SelectObjectInEditor(Item->Object); } return FReply::Handled(); } private: /** The data this row represents. */ FReferenceTreeItemPtr Item; }; TWeakPtr SReferenceTree::SingletonInstance; void SReferenceTree::OpenDialog(UObject* InObject) { if (SingletonInstance.IsValid()) { SingletonInstance.Pin()->RequestDestroyWindow(); } TSharedPtr Window; TSharedPtr ReferenceTreeWidget; Window = SNew(SWindow) .Title(NSLOCTEXT("ReferenceTree", "ReferenceTree_Title", "Reference Tree")) .ClientSize(FVector2D(300.0f, 400.0f)) .SupportsMaximize(false) .SupportsMinimize(false) [SNew(SBorder) .Padding(4.f) .BorderImage(FEditorStyle::GetBrush("ToolPanel.GroupBorder")) [SAssignNew(ReferenceTreeWidget, SReferenceTree) .Object(InObject)]]; ReferenceTreeWidget->SetWindow(Window); SingletonInstance = Window; FSlateApplication::Get().AddWindow(Window.ToSharedRef()); } void SReferenceTree::Construct(const FArguments& InArgs) { bShowScriptRefs = false; TSharedRef HeaderRowWidget = SNew(SHeaderRow) .Visibility(EVisibility::Collapsed) // Quality label column + SHeaderRow::Column(ColumnID_ReferenceLabel) .DefaultLabel(NSLOCTEXT("SoundWaveOptions", "ReferenceColumnLabel", "Reference")) .FillWidth(1.0f); // Build the top menu. TSharedRef CommandList(new FUICommandList()); FMenuBarBuilder MenuBarBuilder(CommandList); { MenuBarBuilder.AddPullDownMenu(NSLOCTEXT("ReferenceTreeView", "View", "View"), NSLOCTEXT("ReferenceTreeView", "View_Tooltip", "View settings for the reference tree."), FNewMenuDelegate::CreateRaw(this, &SReferenceTree::FillViewEntries)); MenuBarBuilder.AddPullDownMenu(NSLOCTEXT("ReferenceTreeView", "Options", "Options"), NSLOCTEXT("ReferenceTreeView", "Options_Tooltip", "Options for the reference tree."), FNewMenuDelegate::CreateRaw(this, &SReferenceTree::FillOptionsEntries)); } this->ChildSlot [SNew(SVerticalBox) + SVerticalBox::Slot() .AutoHeight() [MenuBarBuilder.MakeWidget()] + SVerticalBox::Slot() .FillHeight(1.0f) [SAssignNew(ReferenceTreeView, SReferenceTreeView) // Point the tree to our array of root-level items. Whenever this changes, we'll call RequestTreeRefresh() .TreeItemsSource(&ReferenceTreeRoot) // Called to child items for any given parent item .OnGetChildren(this, &SReferenceTree::OnGetChildrenForReferenceTree) // Generates the actual widget for a list item .OnGenerateRow(this, &SReferenceTree::OnGenerateRowForReferenceTree) // Generates the right click menu. .OnContextMenuOpening(this, &SReferenceTree::BuildMenuWidget) // Header for the tree .HeaderRow(HeaderRowWidget)] + SVerticalBox::Slot() .HAlign(HAlign_Right) .AutoHeight() [SNew(SButton) .Text(NSLOCTEXT("UnrealEd", "OK", "OK")) .OnClicked(this, &SReferenceTree::OnOK_Clicked)]]; PopulateTree(InArgs._Object); FEditorDelegates::MapChange.AddRaw(this, &SReferenceTree::OnEditorMapChange); } SReferenceTree::~SReferenceTree() { FEditorDelegates::MapChange.RemoveAll(this); DestroyGraphAndTree(); } void SReferenceTree::PopulateTree(UObject* InRootObject) { if (InRootObject == NULL) { return; } // Always regenerate. DestroyGraphAndTree(); FArchiveGenerateReferenceGraph GenerateReferenceGraph(ReferenceGraph); // Empty all items currently in the tree. ReferenceTreeRoot.Empty(); // Add a root node tree item ReferenceTreeRoot.Add(MakeShareable(new FReferenceTreeDataContainer(InRootObject))); const FReferenceGraphNode* RootGraphNode = ReferenceGraph.FindRef(InRootObject); bool bIsReferenced = false; if (RootGraphNode) { // For each node that references the root node, recurse over its links to generate the tree. for (TSet::TConstIterator It(RootGraphNode->Links); It; ++It) { FReferenceGraphNode* Link = *It; UObject* Reference = Link->Object; if ((bShowScriptRefs || !Reference->HasAnyFlags(RF_ClassDefaultObject)) && (Reference->IsA(UActorComponent::StaticClass()) || ObjectTools::IsObjectBrowsable(Reference)) && !Reference->HasAnyFlags(RF_Transient)) { bIsReferenced = true; // Skip default objects unless we are showing script references and transient objects. // Populate links to browsable objects and actor components (we will actually display the actor or script reference for components) PopulateTreeRecursive(*Link, ReferenceTreeRoot[0]); } } } // Expand all tree nodes and ensure the root item is visible SetAllExpansionStates(true); ReferenceTreeView->RequestTreeRefresh(); } bool SReferenceTree::PopulateTreeRecursive(FReferenceGraphNode& InNode, FReferenceTreeItemPtr InParentNode) { // Prevent circular references. This node has now been visited for this path. InNode.bVisited = true; bool bNodesWereAdded = false; UObject* ObjectToDisplay = InNode.GetObjectToDisplay(bShowScriptRefs); if (ObjectToDisplay) { // Make a tree node for this object. If the object is a component, display the components outer instead. InParentNode->ChildrenReferences.Add(MakeShareable(new FReferenceTreeDataContainer(ObjectToDisplay))); // We just added a node. Inform the parent. bNodesWereAdded = true; uint32 NumChildrenAdded = 0; // @todo: Move to INI or menu option? const uint32 MaxChildrenPerNodeToDisplay = 50; // Iterate over all this nodes links and add them to the tree for (TSet::TConstIterator It(InNode.Links); It; ++It) { if (NumChildrenAdded == MaxChildrenPerNodeToDisplay) { // The tree is getting too large to be usable // We will display a node saying how many other nodes there are that cant be displayed // @todo: provide the ability to expand this node and populate the tree with the skipped nodes. // stop populating break; } FReferenceGraphNode* Link = *It; UObject* Object = Link->Object; // Only recurse into unvisited nodes which are components or are visible in the content browser. // Components are acceptable so their actor references can be added to the tree. bool bObjectIsValid = !Object->HasAnyFlags(RF_Transient) && (Object->IsA(UActorComponent::StaticClass()) || // Allow actor components to pass so that their actors can be displayed Object->IsA(UPolys::StaticClass()) || // Allow polys to pass so BSP can be displayed ObjectTools::IsObjectBrowsable(Object)); // Allow all browsable objects through if (Link->bVisited == false && bObjectIsValid) { if (PopulateTreeRecursive(*Link, InParentNode->ChildrenReferences[InParentNode->ChildrenReferences.Num() - 1])) { ++NumChildrenAdded; } } } } // We can safely visit this node again, all of its links have been visited. // Any other way this node is visited represents a new path. InNode.bVisited = false; return bNodesWereAdded; } void SReferenceTree::OnGetChildrenForReferenceTree(FReferenceTreeItemPtr InParent, TArray& OutChildren) { // Simply return the children, it's already setup. OutChildren = InParent->ChildrenReferences; } TSharedPtr SReferenceTree::BuildMenuWidget() { // Empty list of commands. TSharedPtr Commands; const bool bShouldCloseWindowAfterMenuSelection = true; // Set the menu to automatically close when the user commits to a choice FMenuBuilder MenuBuilder(bShouldCloseWindowAfterMenuSelection, Commands); { if (ReferenceTreeView->GetSelectedItems().Num()) { UObject* SelectedObject = ReferenceTreeView->GetSelectedItems()[0]->Object; AActor* Actor = Cast(SelectedObject); if (Actor) { FUIAction SelectActorAction(FExecuteAction::CreateStatic(ReferenceTreeView::Helpers::SelectObjectInEditor, SelectedObject)); MenuBuilder.AddMenuEntry(NSLOCTEXT("ReferenceTreeView", "SelectActor", "Select Actor"), NSLOCTEXT("ReferenceTreeView", "SelectActor_Tooltip", "Select the actor in the viewport."), FSlateIcon(), SelectActorAction); FUIAction ViewPropertiesAction(FExecuteAction::CreateRaw(this, &SReferenceTree::OnMenuViewProperties, SelectedObject)); MenuBuilder.AddMenuEntry(NSLOCTEXT("ReferenceTreeView", "ViewProperties", "View Properties"), NSLOCTEXT("ReferenceTreeView", "ViewProperties_Tooltip", "View the actor's properties."), FSlateIcon(), ViewPropertiesAction); } else { FUIAction OpenEditorAction(FExecuteAction::CreateRaw(this, &SReferenceTree::OnMenuShowEditor, SelectedObject)); MenuBuilder.AddMenuEntry(NSLOCTEXT("ReferenceTreeView", "OpenEditor", "Open Editor"), NSLOCTEXT("ReferenceTreeView", "OpenEditor_ToolTip", "Opens the asset's editor."), FSlateIcon(), OpenEditorAction); FUIAction ShowInContentBrowserAction(FExecuteAction::CreateStatic(ReferenceTreeView::Helpers::SelectObjectInEditor, SelectedObject)); MenuBuilder.AddMenuEntry(NSLOCTEXT("ReferenceTreeView", "ShowInContentBrowser", "Show in Content Browser"), NSLOCTEXT("ReferenceTreeView", "ShowInContentBrowser_Tooltip", "Shows the asset in the Content Browser."), FSlateIcon(), ShowInContentBrowserAction); } } } return MenuBuilder.MakeWidget(); } void SReferenceTree::FillViewEntries(FMenuBuilder& MenuBuilder) { MenuBuilder.AddMenuEntry(NSLOCTEXT("ReferenceTreeView", "RebuildTree", "Rebuild Tree"), NSLOCTEXT("ReferenceTreeView", "RebuildTree_Tooltip", "Rebuilds the tree."), FSlateIcon(), FUIAction(FExecuteAction::CreateRaw(this, &SReferenceTree::PopulateTree, ReferenceTreeRoot.Num() ? ReferenceTreeRoot[0]->Object : NULL))); MenuBuilder.AddMenuEntry(NSLOCTEXT("ReferenceTreeView", "CollapseAll", "Collapse All"), NSLOCTEXT("ReferenceTreeView", "CollapseAll_Tooltip", "Collapses all items in the tree."), FSlateIcon(), FUIAction(FExecuteAction::CreateRaw(this, &SReferenceTree::SetAllExpansionStates, false))); MenuBuilder.AddMenuEntry(NSLOCTEXT("ReferenceTreeView", "ExpandAll", "Expand All"), NSLOCTEXT("ReferenceTreeView", "ExpandAll_Tooltip", "Expands all items in the tree."), FSlateIcon(), FUIAction(FExecuteAction::CreateRaw(this, &SReferenceTree::SetAllExpansionStates, true))); } void SReferenceTree::FillOptionsEntries(FMenuBuilder& MenuBuilder) { MenuBuilder.AddMenuEntry(NSLOCTEXT("ReferenceTreeView", "ShowScriptObjects", "Show Script Objects"), NSLOCTEXT("ReferenceTreeView", "ShowScriptObjects_Tooltip", "Toggles displaying script objects in the tree."), FSlateIcon(), FUIAction(FExecuteAction::CreateRaw(this, &SReferenceTree::OnShowScriptReferences), FCanExecuteAction(), FIsActionChecked::CreateRaw(this, &SReferenceTree::OnShowScriptReferences_Checked))); } void SReferenceTree::SetAllExpansionStates(bool bInExpansionState) { // Go through all the items in the root of the tree and recursively visit their children to set every item in the tree. for (int32 ChildIndex = 0; ChildIndex < ReferenceTreeRoot.Num(); ChildIndex++) { SetAllExpansionStates_Helper(ReferenceTreeRoot[ChildIndex], bInExpansionState); } } void SReferenceTree::SetAllExpansionStates_Helper(FReferenceTreeItemPtr InNode, bool bInExpansionState) { ReferenceTreeView->SetItemExpansion(InNode, bInExpansionState); // Recursively go through the children. for (int32 ChildIndex = 0; ChildIndex < InNode->ChildrenReferences.Num(); ChildIndex++) { SetAllExpansionStates_Helper(InNode->ChildrenReferences[ChildIndex], bInExpansionState); } } void SReferenceTree::OnEditorMapChange(uint32 InMapChangeFlags) { if (MapChangeEventFlags::WorldTornDown) { // If a map is changing and the world was torn down, destroy the graph DestroyGraphAndTree(); } } void SReferenceTree::DestroyGraphAndTree() { // Remove all items from the tree ReferenceTreeRoot.Empty(); // Delete every node in the graph. for (FReferenceGraph::TIterator It(ReferenceGraph); It; ++It) { delete It.Value(); It.Value() = NULL; } // Empty graph ReferenceGraph.Empty(); } void SReferenceTree::OnMenuViewProperties(UObject* InObject) { // Show the property windows and create one if necessary GUnrealEd->ShowActorProperties(); // Show the property window for the actor TArray Objects; Objects.Add(InObject); GUnrealEd->UpdateFloatingPropertyWindowsFromActorList(Objects); } void SReferenceTree::OnMenuShowEditor(UObject* InObject) { // Show the editor for this object GEditor->GetEditorSubsystem()->OpenEditorForAsset(InObject); } void SReferenceTree::OnShowScriptReferences() { bShowScriptRefs = !bShowScriptRefs; if (ReferenceTreeRoot.Num()) { PopulateTree(ReferenceTreeRoot[0]->Object); } } FReply SReferenceTree::OnOK_Clicked(void) { MyWindow.Pin()->RequestDestroyWindow(); return FReply::Handled(); } TSharedRef SReferenceTree::OnGenerateRowForReferenceTree(FReferenceTreeItemPtr Item, const TSharedRef& OwnerTable) { return SNew(SReferenceTreeRow, OwnerTable) .Item(Item) .ToolTipText(FText::FromString(Item->Object->GetFullName())); }