// Copyright Epic Games, Inc. All Rights Reserved. #include "SStructViewer.h" #include "StructViewerNode.h" #include "StructViewerFilter.h" #include "StructViewerProjectSettings.h" #include "Misc/PackageName.h" #include "Misc/ScopedSlowTask.h" #include "Modules/ModuleManager.h" #include "UObject/UObjectIterator.h" #include "Misc/TextFilterExpressionEvaluator.h" #include "Editor.h" #include "Styling/AppStyle.h" #include "Styling/SlateIconFinder.h" #include "SlateOptMacros.h" #include "EditorWidgetsModule.h" #include "Framework/Commands/UIAction.h" #include "Framework/Commands/UICommandList.h" #include "Framework/MultiBox/MultiBoxBuilder.h" #include "Framework/Application/SlateApplication.h" #include "Widgets/SOverlay.h" #include "Widgets/SToolTip.h" #include "Widgets/Images/SImage.h" #include "Widgets/Input/SSearchBox.h" #include "Widgets/Layout/SSeparator.h" #include "Widgets/Input/SComboButton.h" #include "Widgets/Layout/SScrollBorder.h" #include "Framework/Docking/TabManager.h" #include "SListViewSelectorDropdownMenu.h" #include "AssetRegistry/ARFilter.h" #include "AssetRegistry/AssetData.h" #include "AssetRegistry/AssetRegistryModule.h" #include "ContentBrowserDataDragDropOp.h" #include "Editor/UnrealEdEngine.h" #include "StructUtils/UserDefinedStruct.h" #include "EditorDirectories.h" #include "Dialogs/Dialogs.h" #include "IDocumentation.h" #include "PropertyHandle.h" #include "Logging/LogMacros.h" #include "SourceCodeNavigation.h" #include "HAL/PlatformApplicationMisc.h" #include "Subsystems/AssetEditorSubsystem.h" #define LOCTEXT_NAMESPACE "SStructViewer" DEFINE_LOG_CATEGORY_STATIC(LogEditorStructViewer, Log, All); EStructFilterReturn FStructViewerFilterFuncs::IfInChildOfStructsSet(TSet& InSet, const UScriptStruct* InStruct) { check(InStruct); if (InSet.Num()) { // If a struct is a child of any structs on this list, it will be allowed onto the list, unless it also appears on a disallowed list. for (const UScriptStruct* CurStruct : InSet) { if (InStruct->IsChildOf(CurStruct)) { return EStructFilterReturn::Passed; } } return EStructFilterReturn::Failed; } // Since there are none on this list, return that there is no items. return EStructFilterReturn::NoItems; } EStructFilterReturn FStructViewerFilterFuncs::IfMatchesAllInChildOfStructsSet(TSet& InSet, const UScriptStruct* InStruct) { check(InStruct); if (InSet.Num()) { // If a struct is a child of any structs on this list, it will be allowed onto the list, unless it also appears on a disallowed list. for (const UScriptStruct* CurStruct : InSet) { if (!InStruct->IsChildOf(CurStruct)) { // Since it doesn't match one, it fails. return EStructFilterReturn::Failed; } } // It matches all of them, so it passes. return EStructFilterReturn::Passed; } // Since there are none on this list, return that there is no items. return EStructFilterReturn::NoItems; } EStructFilterReturn FStructViewerFilterFuncs::IfInStructsSet(TSet& InSet, const UScriptStruct* InStruct) { check(InStruct); if (InSet.Num()) { return InSet.Contains(InStruct) ? EStructFilterReturn::Passed : EStructFilterReturn::Failed; } // Since there are none on this list, return that there is no items. return EStructFilterReturn::NoItems; } class FStructHierarchy { public: FStructHierarchy(); ~FStructHierarchy(); /** Get the singleton instance, creating it if required */ static FStructHierarchy& Get(); /** Get the singleton instance, or null if it doesn't exist */ static FStructHierarchy* GetPtr(); /** Destroy the singleton instance */ static void DestroyInstance(); /** Used to inform any registered Struct Viewers to refresh. */ DECLARE_MULTICAST_DELEGATE(FPopulateStructViewer); FPopulateStructViewer& GetPopulateStructViewerDelegate() { return PopulateStructViewerDelegate; } /** Get the root node of the tree */ TSharedRef GetStructRootNode() const { return StructRootNode.ToSharedRef(); } /** * Finds the node, recursively going deeper into the hierarchy. * @param InRootNode The node to start the search from. * @param InStructPath The path name of the struct to find the node for. * @return The node. */ TSharedPtr FindNodeByStructPath(const TSharedRef& InRootNode, const FSoftObjectPath& InStructPath); TSharedPtr FindNodeByStructPath(const FSoftObjectPath& InStructPath) { return FindNodeByStructPath(GetStructRootNode(), InStructPath); } /** Update the struct hierarchy if it is pending a refresh */ void UpdateStructHierarchy(); private: /** Dirty the struct hierarchy so it will be rebuilt on the next call to UpdateStructHierarchy */ void DirtyStructHierarchy(); /** Populates the struct hierarchy tree, pulling in all the loaded and unloaded structs. */ void PopulateStructHierarchy(); /** * Recursively searches through the hierarchy to find and remove the asset. Used when deleting assets. * * @param InRootNode The node to start the search from. * @param InStructPath The path name of the struct to delete * * @return Returns true if the struct was found and deleted successfully. */ bool FindAndRemoveNodeByStructPath(const TSharedRef& InRootNode, const FSoftObjectPath& InStructPath); bool FindAndRemoveNodeByStructPath(const FSoftObjectPath& InStructPath) { return FindAndRemoveNodeByStructPath(GetStructRootNode(), InStructPath); } /** Callback registered to the Asset Registry to be notified when an asset is added. */ void AddAsset(const FAssetData& InAddedAssetData); /** Callback registered to the Asset Registry to be notified when an asset is removed. */ void RemoveAsset(const FAssetData& InRemovedAssetData); /** Called when reload has finished */ void OnReloadComplete(EReloadCompleteReason Reason); /** Called when modules are loaded or unloaded */ void OnModulesChanged(FName ModuleThatChanged, EModuleChangeReason ReasonForChange); private: /** The struct hierarchy singleton that manages the unfiltered struct tree for the Struct Viewer. */ static TUniquePtr Singleton; /** True if the struct hierarchy should be refreshed */ bool bRefreshStructHierarchy = false; /** Used to inform any registered Struct Viewers to refresh. */ FPopulateStructViewer PopulateStructViewerDelegate; /** The dummy struct data node that is used as a root point for the Struct Viewer. */ TSharedPtr StructRootNode; }; TUniquePtr FStructHierarchy::Singleton; namespace StructViewer { namespace Helpers { /** * Checks if the struct is allowed under the init options of the struct viewer currently building it's tree/list. * @param InInitOptions The struct viewer's options, holds the AllowedStructs and DisallowedStructs. * @param InStruct The struct to test against. */ bool IsStructAllowed(const FStructViewerInitializationOptions& InInitOptions, const TWeakObjectPtr InStruct) { if (InInitOptions.StructFilter.IsValid()) { return InInitOptions.StructFilter->IsStructAllowed(InInitOptions, InStruct.Get(), MakeShared()); } return true; } /** * Checks if the unloaded struct is allowed under the init options of the struct viewer currently building it's tree/list. * @param InInitOptions The struct viewer's options, holds the AllowedStructs and DisallowedStructs. * @param InStructPath The path name to test against. */ bool IsStructAllowed_UnloadedStruct(const FStructViewerInitializationOptions& InInitOptions, const FSoftObjectPath& InStructPath) { if (InInitOptions.StructFilter.IsValid()) { return InInitOptions.StructFilter->IsUnloadedStructAllowed(InInitOptions, InStructPath, MakeShared()); } return true; } /** * Checks if the TestString passes the filter. * @param InTestString The string to test against the filter. * @param InTextFilter Compiled text filter to apply. * @return true if it passes the filter. */ bool PassesFilter(const FString& InTestString, const FTextFilterExpressionEvaluator& InTextFilter) { return InTextFilter.TestTextFilter(FBasicStringFilterExpressionContext(InTestString)); } /** * Recursive function to build a tree, filtering out nodes based on the InitOptions and filter search terms. * @param InInitOptions The struct viewer's options, holds the AllowedStructs and DisallowedStructs. * @param InOutRootNode The node that this function will add the children of to the tree. * @param InOriginalRootNode The original root node holding the data we produce a filtered node for. * @param InTextFilter Compiled text filter to apply. * @param bInShowUnloadedStructs Filter option to not remove unloaded structs due to struct filter options. * @param InAllowedDeveloperType Filter option for dealing with developer folders. * @param bInInternalStructs Filter option for showing internal structs. * @param InternalStructs The structs that have been marked as Internal Only. * @param InternalPaths The paths that have been marked Internal Only. * @return Returns true if the child passed the filter. */ bool AddChildren_Tree( const FStructViewerInitializationOptions& InInitOptions, const TSharedRef& InOutRootNode, const TSharedRef& InOriginalRootNode, const FTextFilterExpressionEvaluator& InTextFilter, const bool bInShowUnloadedStructs, const EStructViewerDeveloperType InAllowedDeveloperType, const bool bInInternalStructs, const TArray& InternalStructs, const TArray& InternalPaths ) { static const FString DeveloperPathWithSlash = FPackageName::FilenameToLongPackageName(FPaths::GameDevelopersDir()); static const FString UserDeveloperPathWithSlash = FPackageName::FilenameToLongPackageName(FPaths::GameUserDeveloperDir()); const UScriptStruct* OriginalRootNodeStruct = InOriginalRootNode->GetStruct(); // Determine if we allow any developer folder classes, if so determine if this struct is in one of the allowed developer folders. const FString GeneratedStructPathString = InOriginalRootNode->GetStructPath().ToString(); bool bPassesDeveloperFilter = true; if (InAllowedDeveloperType == EStructViewerDeveloperType::SVDT_None) { bPassesDeveloperFilter = !GeneratedStructPathString.StartsWith(DeveloperPathWithSlash); } else if (InAllowedDeveloperType == EStructViewerDeveloperType::SVDT_CurrentUser) { if (GeneratedStructPathString.StartsWith(DeveloperPathWithSlash)) { bPassesDeveloperFilter = GeneratedStructPathString.StartsWith(UserDeveloperPathWithSlash); } } // The INI files declare structs and folders that are considered internal only. Does this struct match any of those patterns? // INI path: /Script/StructViewer.StructViewerProjectSettings bool bPassesInternalFilter = true; if (!bInInternalStructs && InternalPaths.Num() > 0) { for (const FDirectoryPath& InternalPath : InternalPaths) { if (GeneratedStructPathString.StartsWith(InternalPath.Path)) { bPassesInternalFilter = false; break; } } } if (!bInInternalStructs && InternalStructs.Num() > 0 && bPassesInternalFilter && OriginalRootNodeStruct) { for (const UScriptStruct* InternalStruct : InternalStructs) { if (OriginalRootNodeStruct->IsChildOf(InternalStruct)) { bPassesInternalFilter = false; break; } } } // There are few options for filtering an unloaded struct, if it matches with this filter, it passes. bool bReturnPassesFilter = false; if (OriginalRootNodeStruct) { bReturnPassesFilter = bPassesDeveloperFilter && bPassesInternalFilter && IsStructAllowed(InInitOptions, OriginalRootNodeStruct) && (PassesFilter(InOriginalRootNode->GetStructName(), InTextFilter) || PassesFilter(InOriginalRootNode->GetStructDisplayName().ToString(), InTextFilter)); } else { if (bInShowUnloadedStructs) { bReturnPassesFilter = bPassesDeveloperFilter && bPassesInternalFilter && IsStructAllowed_UnloadedStruct(InInitOptions, InOutRootNode->GetStructPath()) && (PassesFilter(InOriginalRootNode->GetStructName(), InTextFilter) || PassesFilter(InOriginalRootNode->GetStructDisplayName().ToString(), InTextFilter)); } } InOutRootNode->PassedFilter(bReturnPassesFilter); for (const TSharedPtr& ChildNode : InOriginalRootNode->GetChildNodes()) { TSharedRef NewNode = MakeShared(ChildNode.ToSharedRef(), InInitOptions.PropertyHandle, /*bPassedFilter*/false); // Whether we pass the filter is calculated in the recursive call const bool bChildrenPassesFilter = AddChildren_Tree(InInitOptions, NewNode, ChildNode.ToSharedRef(), InTextFilter, bInShowUnloadedStructs, InAllowedDeveloperType, bInInternalStructs, InternalStructs, InternalPaths); bReturnPassesFilter |= bChildrenPassesFilter; if (bChildrenPassesFilter) { InOutRootNode->AddChild(NewNode); } } return bReturnPassesFilter; } /** * Builds the struct tree. * @param InInitOptions The struct viewer's options, holds the AllowedStructs and DisallowedStructs. * @param InOutRootNode The node to root the tree to. * @param InTextFilter Compiled text filter to apply. * @param bInShowUnloadedStructs Filter option to not remove unloaded structs due to struct filter options. * @param InAllowedDeveloperType Filter option for dealing with developer folders. * @param bInInternalStructs Filter option for showing internal structs. * @param InternalStructs The structs that have been marked as Internal Only. * @param InternalPaths The paths that have been marked Internal Only. * @return A fully built tree. */ void GetStructTree( const FStructViewerInitializationOptions& InInitOptions, TSharedPtr& InOutRootNode, const FTextFilterExpressionEvaluator& InTextFilter, const bool bInShowUnloadedStructs, const EStructViewerDeveloperType InAllowedDeveloperType = EStructViewerDeveloperType::SVDT_All, const bool bInInternalStructs = true, const TArray& InternalStructs = TArray(), const TArray& InternalPaths = TArray() ) { const TSharedRef StructRootNode = FStructHierarchy::Get().GetStructRootNode(); // Make a dummy root node InOutRootNode = MakeShared(); AddChildren_Tree(InInitOptions, InOutRootNode.ToSharedRef(), StructRootNode, InTextFilter, bInShowUnloadedStructs, InAllowedDeveloperType, bInInternalStructs, InternalStructs, InternalPaths); } /** * Recursive function to build the list, filtering out nodes based on the InitOptions and filter search terms. * @param InInitOptions The struct viewer's options, holds the AllowedStructs and DisallowedStructs. * @param InOutNodeList The list to add all the nodes to. * @param InOriginalRootNode The original root node holding the data we produce a filtered node for. * @param InTextFilter Compiled text filter to apply. * @param bInShowUnloadedStructs Filter option to not remove unloaded structs due to struct filter options. * @param InAllowedDeveloperType Filter option for dealing with developer folders. * @param bInInternalStructs Filter option for showing internal structs. * @param InternalStructs The structs that have been marked as Internal Only. * @param InternalPaths The paths that have been marked Internal Only. * @return Returns true if the child passed the filter. */ void AddChildren_List( const FStructViewerInitializationOptions& InInitOptions, TArray>& InOutNodeList, const TSharedRef& InOriginalRootNode, const FTextFilterExpressionEvaluator& InTextFilter, const bool bInShowUnloadedStructs, const EStructViewerDeveloperType InAllowedDeveloperType, const bool bInInternalStructs, const TArray& InternalStructs, const TArray& InternalPaths ) { static const FString DeveloperPathWithSlash = FPackageName::FilenameToLongPackageName(FPaths::GameDevelopersDir()); static const FString UserDeveloperPathWithSlash = FPackageName::FilenameToLongPackageName(FPaths::GameUserDeveloperDir()); const UScriptStruct* OriginalRootNodeStruct = InOriginalRootNode->GetStruct(); // Determine if we allow any developer folder structs, if so determine if this struct is in one of the allowed developer folders. FString GeneratedStructPathString = InOriginalRootNode->GetStructPath().ToString(); bool bPassesDeveloperFilter = true; if (InAllowedDeveloperType == EStructViewerDeveloperType::SVDT_None) { bPassesDeveloperFilter = !GeneratedStructPathString.StartsWith(DeveloperPathWithSlash); } else if (InAllowedDeveloperType == EStructViewerDeveloperType::SVDT_CurrentUser) { if (GeneratedStructPathString.StartsWith(DeveloperPathWithSlash)) { bPassesDeveloperFilter = GeneratedStructPathString.StartsWith(UserDeveloperPathWithSlash); } } // The INI files declare structs and folders that are considered internal only. Does this struct match any of those patterns? // INI path: /Script/StructViewer.StructViewerProjectSettings bool bPassesInternalFilter = true; if (!bInInternalStructs && InternalPaths.Num() > 0) { for (const FDirectoryPath& InternalPath : InternalPaths) { if (GeneratedStructPathString.StartsWith(InternalPath.Path)) { bPassesInternalFilter = false; break; } } } if (!bInInternalStructs && InternalStructs.Num() > 0 && bPassesInternalFilter && OriginalRootNodeStruct) { for (const UScriptStruct* InternalStruct : InternalStructs) { if (OriginalRootNodeStruct->IsChildOf(InternalStruct)) { bPassesInternalFilter = false; break; } } } // There are few options for filtering an unloaded struct, if it matches with this filter, it passes. bool bPassedFilter = false; if (OriginalRootNodeStruct) { bPassedFilter = bPassesDeveloperFilter && bPassesInternalFilter && IsStructAllowed(InInitOptions, OriginalRootNodeStruct) && (PassesFilter(InOriginalRootNode->GetStructName(), InTextFilter) || PassesFilter(InOriginalRootNode->GetStructDisplayName().ToString(), InTextFilter)); } else { if (bInShowUnloadedStructs) { bPassedFilter = bPassesDeveloperFilter && bPassesInternalFilter && IsStructAllowed_UnloadedStruct(InInitOptions, InOriginalRootNode->GetStructPath()) && (PassesFilter(InOriginalRootNode->GetStructName(), InTextFilter) || PassesFilter(InOriginalRootNode->GetStructDisplayName().ToString(), InTextFilter)); } } if (bPassedFilter) { InOutNodeList.Add(MakeShared(InOriginalRootNode, InInitOptions.PropertyHandle, bPassedFilter)); } for (const TSharedPtr& ChildNode : InOriginalRootNode->GetChildNodes()) { AddChildren_List(InInitOptions, InOutNodeList, ChildNode.ToSharedRef(), InTextFilter, bInShowUnloadedStructs, InAllowedDeveloperType, bInInternalStructs, InternalStructs, InternalPaths); } } /** * Builds the struct list. * @param InInitOptions The struct viewer's options, holds the AllowedStructs and DisallowedStructs. * @param InOutNodeList The list to add all the nodes to. * @param InTextFilter Compiled text filter to apply. * @param bInShowUnloadedStructs Filter option to not remove unloaded structs due to struct filter options. * @param InAllowedDeveloperType Filter option for dealing with developer folders. * @param bInInternalStructs Filter option for showing internal structs. * @param InternalStructs The structs that have been marked as Internal Only. * @param InternalPaths The paths that have been marked Internal Only. * @return A fully built list. */ void GetStructList( const FStructViewerInitializationOptions& InInitOptions, TArray>& InOutNodeList, const FTextFilterExpressionEvaluator& InTextFilter, const bool bInShowUnloadedStructs, const EStructViewerDeveloperType InAllowedDeveloperType = EStructViewerDeveloperType::SVDT_All, const bool bInInternalStructs = true, const TArray& InternalStructs = TArray(), const TArray& InternalPaths = TArray() ) { const TSharedRef StructRootNode = FStructHierarchy::Get().GetStructRootNode(); // Always skip the dummy root, only adding its children for (const TSharedPtr& ChildNode : StructRootNode->GetChildNodes()) { AddChildren_List(InInitOptions, InOutNodeList, ChildNode.ToSharedRef(), InTextFilter, bInShowUnloadedStructs, InAllowedDeveloperType, bInInternalStructs, InternalStructs, InternalPaths); } } /** * Opens an asset editor for a user defined struct. */ void OpenAssetEditor(const UUserDefinedStruct* InStruct) { if (InStruct) { GEditor->GetEditorSubsystem()->OpenEditorForAsset(const_cast(InStruct)); } } /** * Opens a struct source file. */ void OpenStructInIDE(const UScriptStruct* InStruct) { FSourceCodeNavigation::NavigateToStruct(InStruct); } /** * Copy struct FTopLevelAssetPath to clipboard. */ void CopyStructPathToClipboard(const UScriptStruct* InStruct) { if (InStruct) { FPlatformApplicationMisc::ClipboardCopy(*InStruct->GetStructPathName().ToString()); } } /** * Finds the struct in the content browser. */ void FindInContentBrowser(const UScriptStruct* InStruct) { if (InStruct) { TArray Objects; Objects.Add(const_cast(InStruct)); GEditor->SyncBrowserToObjects(Objects); } } TSharedRef CreateMenu(const UScriptStruct* InStruct) { // 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 (const UUserDefinedStruct* UserDefinedStruct = Cast(InStruct)) { MenuBuilder.AddMenuEntry( LOCTEXT("StructViewerMenuEditStructAsset", "Edit Struct..."), LOCTEXT("StructViewerMenuEditStructAsset_Tooltip", "Open the struct in the asset editor."), FSlateIcon(), FUIAction(FExecuteAction::CreateStatic(&StructViewer::Helpers::OpenAssetEditor, UserDefinedStruct)) ); MenuBuilder.AddMenuEntry( LOCTEXT("StructViewerMenuFindContent", "Find in Content Browser..."), LOCTEXT("StructViewerMenuFindContent_Tooltip", "Find in Content Browser"), FSlateIcon(), FUIAction(FExecuteAction::CreateStatic(&StructViewer::Helpers::FindInContentBrowser, InStruct)) ); } else { MenuBuilder.AddMenuEntry( LOCTEXT("StructViewerMenuOpenSourceCode", "Open Source Code..."), LOCTEXT("StructViewerMenuOpenSourceCode_Tooltip", "Open the source file for this struct in the IDE."), FSlateIcon(), FUIAction(FExecuteAction::CreateStatic(&StructViewer::Helpers::OpenStructInIDE, InStruct)) ); } MenuBuilder.AddMenuEntry( LOCTEXT("StructViewerMenuCopyStructPath", "Copy Struct Path"), LOCTEXT("StructViewerMenuCopyStructPath_Tooltip", "Copies the path for this struct (e.g '/Script/Module.StructName') to the clipboard."), FSlateIcon(), FUIAction(FExecuteAction::CreateStatic(&StructViewer::Helpers::CopyStructPathToClipboard, InStruct)) ); } return MenuBuilder.MakeWidget(); } } // namespace Helpers } // namespace StructViewer /** Delegate used with the Struct Viewer in 'struct picking' mode. You'll bind a delegate when the struct viewer widget is created, which will be fired off when the selected struct is double clicked */ DECLARE_DELEGATE_OneParam(FOnStructItemDoubleClickDelegate, TSharedPtr); /** The item used for visualizing the struct in the tree. */ class SStructItem : public STableRow> { public: SLATE_BEGIN_ARGS(SStructItem) : _StructDisplayName() , _bIsInStructViewer(true) , _bDynamicStructLoading(true) , _HighlightText() , _TextColor(FLinearColor(1.0f, 1.0f, 1.0f, 1.0f)) {} /** The struct name this item contains. */ SLATE_ARGUMENT(FText, StructDisplayName) /** true if this item is in a Struct Viewer (as opposed to a Struct Picker) */ SLATE_ARGUMENT(bool, bIsInStructViewer) /** true if this item should allow dynamic struct loading */ SLATE_ARGUMENT(bool, bDynamicStructLoading) /** The text this item should highlight, if any. */ SLATE_ARGUMENT(FText, HighlightText) /** The color text this item will use. */ SLATE_ARGUMENT(FSlateColor, TextColor) /** The node this item is associated with. */ SLATE_ARGUMENT(TSharedPtr, AssociatedNode) /** the delegate for handling double clicks outside of the SStructItem */ SLATE_ARGUMENT(FOnStructItemDoubleClickDelegate, OnStructItemDoubleClicked) /** On Struct Picked callback. */ SLATE_EVENT(FOnDragDetected, OnDragDetected) SLATE_END_ARGS() /** * Construct the widget * * @param InArgs A declaration from which to construct the widget */ void Construct(const FArguments& InArgs, const TSharedRef& InOwnerTableView) { StructDisplayName = InArgs._StructDisplayName; bIsInStructViewer = InArgs._bIsInStructViewer; bDynamicStructLoading = InArgs._bDynamicStructLoading; AssociatedNode = InArgs._AssociatedNode; OnDoubleClicked = InArgs._OnStructItemDoubleClicked; auto BuildToolTip = [this]() -> TSharedPtr { TSharedPtr ToolTip; TSharedPtr PropertyHandle = AssociatedNode->GetPropertyHandle(); if (PropertyHandle && AssociatedNode->IsRestricted()) { FText RestrictionToolTip; PropertyHandle->GenerateRestrictionToolTip(*AssociatedNode->GetStructName(), RestrictionToolTip); ToolTip = SNew(SToolTip).Text(RestrictionToolTip); } else if (!AssociatedNode->GetStructPath().IsNull()) { const UScriptStruct* Struct = AssociatedNode->GetStruct(); if (Struct != nullptr && Struct->GetBoolMetaDataHierarchical("ShowTooltip")) { const FText ToolTipText = FText::Format(LOCTEXT("ToolTipFormat", "{0}\n\n{1}"), AssociatedNode->GetStruct()->GetToolTipText(), FText::FromString(AssociatedNode->GetStructPath().ToString()) ); ToolTip = SNew(SToolTip).Text(ToolTipText); } else { ToolTip = SNew(SToolTip).Text(FText::FromString(AssociatedNode->GetStructPath().ToString())); } } return ToolTip; }; const bool bIsRestricted = AssociatedNode->IsRestricted(); this->ChildSlot [ SNew(SHorizontalBox) +SHorizontalBox::Slot() .AutoWidth() [ SNew(SExpanderArrow, SharedThis(this)) ] +SHorizontalBox::Slot() .AutoWidth() .Padding(0.0f, 0.0f, 6.0f, 0.0f) .VAlign(VAlign_Center) [ SNew(SImage) .Image(this, &SStructItem::GetIconImage) .Visibility(this, &SStructItem::GetIconVisibility) ] +SHorizontalBox::Slot() .FillWidth(1.0f) .Padding(0.0f, 3.0f, 6.0f, 3.0f) .VAlign(VAlign_Center) [ SNew(STextBlock) .Text(StructDisplayName) .HighlightText(InArgs._HighlightText) .ColorAndOpacity(this, &SStructItem::GetTextColor) .ToolTip(BuildToolTip()) .IsEnabled(!bIsRestricted) ] +SHorizontalBox::Slot() .AutoWidth() .HAlign(HAlign_Right) .VAlign(VAlign_Center) .Padding(0.0f, 0.0f, 6.0f, 0.0f) [ SNew(SComboButton) .ContentPadding(FMargin(2.0f)) .Visibility(this, &SStructItem::ShowOptions) .OnGetMenuContent(this, &SStructItem::GenerateDropDown) ] ]; TextColor = InArgs._TextColor; UE_LOG(LogEditorStructViewer, VeryVerbose, TEXT("STRUCT [%s]"), *StructDisplayName.ToString()); STableRow>::ConstructInternal( STableRow::FArguments() .ShowSelection(true) .OnDragDetected(InArgs._OnDragDetected), InOwnerTableView ); } private: FReply OnMouseButtonDoubleClick(const FGeometry& InMyGeometry, const FPointerEvent& InMouseEvent) { // If in a Struct Viewer and it has not been loaded, load the struct when double-left clicking. if (bIsInStructViewer) { if (bDynamicStructLoading && InMouseEvent.GetEffectingButton() == EKeys::LeftMouseButton) { AssociatedNode->LoadStruct(); } // If there is a struct asset, open its asset editor; otherwise try to open the struct header if (const UUserDefinedStruct* UserDefinedStruct = AssociatedNode->GetStructAsset()) { StructViewer::Helpers::OpenAssetEditor(UserDefinedStruct); } else if (const UScriptStruct* Struct = AssociatedNode->GetStruct()) { StructViewer::Helpers::OpenStructInIDE(Struct); } } else { OnDoubleClicked.ExecuteIfBound(AssociatedNode); } return FReply::Handled(); } EVisibility ShowOptions() const { // If it's in viewer mode, show the options combo button. return (bIsInStructViewer && AssociatedNode->GetStruct()) ? EVisibility::Visible : EVisibility::Collapsed; } /** * Generates the drop down menu for the item. * * @return The drop down menu widget. */ TSharedRef GenerateDropDown() { if (const UScriptStruct* Struct = AssociatedNode->GetStruct()) { return StructViewer::Helpers::CreateMenu(Struct); } return SNullWidget::NullWidget; } /** Returns the text color for the item based on if it is selected or not. */ FSlateColor GetTextColor() const { const TSharedPtr>> OwnerWidget = OwnerTablePtr.Pin(); const TSharedPtr* MyItem = OwnerWidget->Private_ItemFromWidget(this); const bool bIsSelected = OwnerWidget->Private_IsItemSelected(*MyItem); if (bIsSelected) { return FSlateColor::UseForeground(); } return TextColor; } const FSlateBrush* GetIconImage() const { if (!AssociatedNode) { return nullptr; } return FSlateIconFinder::FindIconBrushForClass(AssociatedNode->GetStruct(), "ClassIcon.Object"); } EVisibility GetIconVisibility() const { const UScriptStruct* Struct = AssociatedNode ? AssociatedNode->GetStruct() : nullptr; return Struct && Struct->GetBoolMetaDataHierarchical("ShowIcon") && GetIconImage() ? EVisibility::Visible : EVisibility::Collapsed; } private: /** The struct name for which this item is associated with. */ FText StructDisplayName; /** true if in a Struct Viewer (as opposed to a Struct Picker). */ bool bIsInStructViewer; /** true if dynamic struct loading is permitted. */ bool bDynamicStructLoading; /** The text color for this item. */ FSlateColor TextColor; /** The Struct Viewer Node this item is associated with. */ TSharedPtr AssociatedNode; /** the on Double Clicked delegate */ FOnStructItemDoubleClickDelegate OnDoubleClicked; }; FStructHierarchy::FStructHierarchy() { // Register with the Asset Registry to be informed when it is done loading up files. FAssetRegistryModule& AssetRegistryModule = FModuleManager::GetModuleChecked(TEXT("AssetRegistry")); AssetRegistryModule.Get().OnFilesLoaded().AddRaw(this, &FStructHierarchy::DirtyStructHierarchy); AssetRegistryModule.Get().OnAssetAdded().AddRaw(this, &FStructHierarchy::AddAsset); AssetRegistryModule.Get().OnAssetRemoved().AddRaw(this, &FStructHierarchy::RemoveAsset); // Register to have Populate called when doing a Reload. FCoreUObjectDelegates::ReloadCompleteDelegate.AddRaw(this, &FStructHierarchy::OnReloadComplete); FModuleManager::Get().OnModulesChanged().AddRaw(this, &FStructHierarchy::OnModulesChanged); PopulateStructHierarchy(); } FStructHierarchy::~FStructHierarchy() { // Unregister with the Asset Registry to be informed when it is done loading up files. if (FModuleManager::Get().IsModuleLoaded(TEXT("AssetRegistry"))) { IAssetRegistry* AssetRegistry = FModuleManager::GetModuleChecked(TEXT("AssetRegistry")).TryGet(); if (AssetRegistry) { AssetRegistry->OnFilesLoaded().RemoveAll(this); AssetRegistry->OnAssetAdded().RemoveAll(this); AssetRegistry->OnAssetRemoved().RemoveAll(this); } // Unregister to have Populate called when doing a Reload. FCoreUObjectDelegates::ReloadCompleteDelegate.RemoveAll(this); } FModuleManager::Get().OnModulesChanged().RemoveAll(this); } FStructHierarchy& FStructHierarchy::Get() { if (!Singleton) { Singleton = MakeUnique(); Singleton->PopulateStructHierarchy(); } return *Singleton; } FStructHierarchy* FStructHierarchy::GetPtr() { return Singleton.Get(); } void FStructHierarchy::DestroyInstance() { Singleton.Reset(); } void FStructHierarchy::UpdateStructHierarchy() { if (bRefreshStructHierarchy) { bRefreshStructHierarchy = false; PopulateStructHierarchy(); } } void FStructHierarchy::DirtyStructHierarchy() { bRefreshStructHierarchy = true; } void FStructHierarchy::PopulateStructHierarchy() { FScopedSlowTask SlowTask(0.0f, LOCTEXT("RebuildingStructHierarchy", "Rebuilding Struct Hierarchy")); SlowTask.MakeDialog(); // Make a dummy root node StructRootNode = MakeShared(); // Add the tree of native structs { TMap> DataNodes; DataNodes.Add(nullptr, StructRootNode); TSet Visited; UPackage* TransientPackage = GetTransientPackage(); // Go through all of the structs and see if they should be added to the list. for (TObjectIterator StructIt; StructIt; ++StructIt) { const UScriptStruct* CurrentStruct = *StructIt; if (Visited.Contains(CurrentStruct) || CurrentStruct->GetOutermost() == TransientPackage) { // Skip transient structs as they are dead leftovers from user struct editing continue; } while (CurrentStruct) { const UScriptStruct* SuperStruct = Cast(CurrentStruct->GetSuperStruct()); TSharedPtr& ParentEntryRef = DataNodes.FindOrAdd(SuperStruct); if (!ParentEntryRef.IsValid()) { check(SuperStruct); // The null entry should have been created above ParentEntryRef = MakeShared(SuperStruct); } // Need to have a pointer in-case the ref moves to avoid an extra look up in the map TSharedPtr ParentEntry = ParentEntryRef; TSharedPtr& MyEntryRef = DataNodes.FindOrAdd(CurrentStruct); if (!MyEntryRef.IsValid()) { MyEntryRef = MakeShared(CurrentStruct); } // Need to re-acquire the reference in the struct as it may have been invalidated from a re-size if (!Visited.Contains(CurrentStruct)) { ParentEntry->AddChild(MyEntryRef.ToSharedRef()); Visited.Add(CurrentStruct); } CurrentStruct = SuperStruct; } } } // Add any struct assets directly under the root (since they don't support inheritance) { FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked(TEXT("AssetRegistry")); FARFilter Filter; Filter.ClassPaths.Add(UUserDefinedStruct::StaticClass()->GetClassPathName()); Filter.bRecursiveClasses = true; TArray UserDefinedStructsList; AssetRegistryModule.Get().GetAssets(Filter, UserDefinedStructsList); for (const FAssetData& UserDefinedStructData : UserDefinedStructsList) { if (!UserDefinedStructData.IsAssetLoaded()) { // If the asset is loaded it was added by the object iterator StructRootNode->AddChild(MakeShared(UserDefinedStructData)); } } } // All viewers must refresh. PopulateStructViewerDelegate.Broadcast(); } TSharedPtr FStructHierarchy::FindNodeByStructPath(const TSharedRef& InRootNode, const FSoftObjectPath& InStructPath) { // Check if the current node is the struct path that is being searched for if (InRootNode->GetStructPath() == InStructPath) { return InRootNode; } // Search the children recursively, one of them might have the node for (const TSharedPtr& ChildNode : InRootNode->GetChildNodes()) { // Check the child, then check the return to see if it is valid. If it is valid, end the recursion. TSharedPtr ReturnNode = FindNodeByStructPath(ChildNode.ToSharedRef(), InStructPath); if (ReturnNode) { return ReturnNode; } } return nullptr; } bool FStructHierarchy::FindAndRemoveNodeByStructPath(const TSharedRef& InRootNode, const FSoftObjectPath& InStructPath) { // Check if the current node contains a child of struct path that is being searched for if (InRootNode->RemoveChild(InStructPath)) { return true; } // Search the children recursively, one of them might have the node for (const TSharedPtr& ChildNode : InRootNode->GetChildNodes()) { // Check the child, then check the return to see if it is valid. If it is valid, end the recursion. if (FindAndRemoveNodeByStructPath(ChildNode.ToSharedRef(), InStructPath)) { return true; } } return false; } void FStructHierarchy::AddAsset(const FAssetData& InAddedAssetData) { FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked(TEXT("AssetRegistry")); if (!AssetRegistryModule.Get().IsLoadingAssets()) { // Only handle structs UClass* AssetClass = InAddedAssetData.GetClass(); if (AssetClass && AssetClass->IsChildOf(UScriptStruct::StaticClass())) { // Make sure that the node does not already exist. There is a bit of double adding going on at times and this prevents it. if (!FindNodeByStructPath(InAddedAssetData.GetSoftObjectPath())) { // User defined structs are always root level structs StructRootNode->AddChild(MakeShared(InAddedAssetData)); // All Viewers must repopulate. PopulateStructViewerDelegate.Broadcast(); } } } } void FStructHierarchy::RemoveAsset(const FAssetData& InRemovedAssetData) { if (FindAndRemoveNodeByStructPath(InRemovedAssetData.GetSoftObjectPath())) { // All viewers must refresh. PopulateStructViewerDelegate.Broadcast(); } } void FStructHierarchy::OnReloadComplete(EReloadCompleteReason Reason) { DirtyStructHierarchy(); } void FStructHierarchy::OnModulesChanged(FName ModuleThatChanged, EModuleChangeReason ReasonForChange) { if (ReasonForChange == EModuleChangeReason::ModuleLoaded || ReasonForChange == EModuleChangeReason::ModuleUnloaded) { DirtyStructHierarchy(); } } BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION void SStructViewer::Construct(const FArguments& InArgs, const FStructViewerInitializationOptions& InInitOptions) { bNeedsRefresh = true; NumStructs = 0; bCanShowInternalStructs = true; // Listen for when view settings are changed UStructViewerSettings::OnSettingChanged().AddSP(this, &SStructViewer::HandleSettingChanged); InitOptions = InInitOptions; OnStructPicked = InArgs._OnStructPickedDelegate; TextFilterPtr = MakeShareable(new FTextFilterExpressionEvaluator(ETextFilterExpressionEvaluatorMode::BasicString)); bSaveExpansionStates = true; bPendingSetExpansionStates = false; bEnableStructDynamicLoading = InInitOptions.bEnableStructDynamicLoading; EVisibility HeaderVisibility = (this->InitOptions.Mode == EStructViewerMode::StructBrowsing) ? EVisibility::Visible : EVisibility::Collapsed; // Set these values to the user specified settings. bShowUnloadedStructs = InitOptions.bShowUnloadedStructs; bool bHasTitle = InitOptions.ViewerTitleString.IsEmpty() == false; // If set to default, decide what display mode to use. if (InitOptions.DisplayMode == EStructViewerDisplayMode::DefaultView) { // By default the Browser uses the tree view, the Picker the list. The option is available to users to force to another display mode when creating the Struct Browser/Picker. if (InitOptions.Mode == EStructViewerMode::StructBrowsing) { InitOptions.DisplayMode = EStructViewerDisplayMode::TreeView; } else { InitOptions.DisplayMode = EStructViewerDisplayMode::ListView; } } // Create the asset discovery indicator FEditorWidgetsModule& EditorWidgetsModule = FModuleManager::LoadModuleChecked("EditorWidgets"); TSharedRef AssetDiscoveryIndicator = EditorWidgetsModule.CreateAssetDiscoveryIndicator(EAssetDiscoveryIndicatorScaleMode::Scale_Vertical); FOnContextMenuOpening OnContextMenuOpening; if (InitOptions.Mode == EStructViewerMode::StructBrowsing) { OnContextMenuOpening = FOnContextMenuOpening::CreateSP(this, &SStructViewer::BuildMenuWidget); } SAssignNew(StructList, SListView>) .SelectionMode(ESelectionMode::Single) .ListItemsSource(&RootTreeItems) // Generates the actual widget for a tree item .OnGenerateRow(this, &SStructViewer::OnGenerateRowForStructViewer) // Generates the right click menu. .OnContextMenuOpening(OnContextMenuOpening) // Find out when the user selects something in the tree .OnSelectionChanged(this, &SStructViewer::OnStructViewerSelectionChanged) .HeaderRow ( SNew(SHeaderRow) .Visibility(EVisibility::Collapsed) +SHeaderRow::Column(TEXT("Struct")) .DefaultLabel(NSLOCTEXT("StructViewer", "Struct", "Struct")) ); SAssignNew(StructTree, STreeView>) .SelectionMode(ESelectionMode::Single) .TreeItemsSource(&RootTreeItems) // Called to child items for any given parent item .OnGetChildren(this, &SStructViewer::OnGetChildrenForStructViewerTree) // Called to handle recursively expanding/collapsing items .OnSetExpansionRecursive(this, &SStructViewer::SetAllExpansionStates_Helper) // Generates the actual widget for a tree item .OnGenerateRow(this, &SStructViewer::OnGenerateRowForStructViewer) // Generates the right click menu. .OnContextMenuOpening(OnContextMenuOpening) // Find out when the user selects something in the tree .OnSelectionChanged(this, &SStructViewer::OnStructViewerSelectionChanged) // Called when the expansion state of an item changes .OnExpansionChanged(this, &SStructViewer::OnStructViewerExpansionChanged) .HeaderRow ( SNew(SHeaderRow) .Visibility(EVisibility::Collapsed) + SHeaderRow::Column(TEXT("Struct")) .DefaultLabel(NSLOCTEXT("StructViewer", "Struct", "Struct")) ); TSharedRef>> StructTreeView = StructTree.ToSharedRef(); TSharedRef>> StructListView = StructList.ToSharedRef(); // Holds the bulk of the struct viewer's sub-widgets, to be added to the widget after construction TSharedPtr StructViewerContent = SNew(SBox) .MaxDesiredHeight(800.0f) [ SNew(SBorder) .BorderImage(FAppStyle::GetBrush(InitOptions.bShowBackgroundBorder ? "ToolPanel.GroupBorder" : "NoBorder")) [ SNew(SVerticalBox) +SVerticalBox::Slot() .AutoHeight() [ SNew(SHorizontalBox) +SHorizontalBox::Slot() .AutoWidth() .VAlign(VAlign_Center) [ SNew(STextBlock) .Visibility(bHasTitle ? EVisibility::Visible : EVisibility::Collapsed) .ColorAndOpacity(FAppStyle::GetColor("MultiboxHookColor")) .Text(InitOptions.ViewerTitleString) ] ] +SVerticalBox::Slot() .AutoHeight() [ SNew(SHorizontalBox) +SHorizontalBox::Slot() .Padding(2.0f, 2.0f) [ SAssignNew(SearchBox, SSearchBox) .OnTextChanged(this, &SStructViewer::OnFilterTextChanged) .OnTextCommitted(this, &SStructViewer::OnFilterTextCommitted) ] ] +SVerticalBox::Slot() .AutoHeight() [ SNew(SSeparator) .Visibility(HeaderVisibility) ] +SVerticalBox::Slot() .FillHeight(1.0f) [ SNew(SOverlay) +SOverlay::Slot() .HAlign(HAlign_Fill) .VAlign(VAlign_Fill) [ SNew(SVerticalBox) +SVerticalBox::Slot() .FillHeight(1.0f) [ SNew(SScrollBorder, StructTreeView) .Visibility(InitOptions.DisplayMode == EStructViewerDisplayMode::TreeView ? EVisibility::Visible : EVisibility::Collapsed) [ StructTreeView ] ] +SVerticalBox::Slot() .FillHeight(1.0f) [ SNew(SScrollBorder, StructListView) .Visibility(InitOptions.DisplayMode == EStructViewerDisplayMode::ListView ? EVisibility::Visible : EVisibility::Collapsed) [ StructListView ] ] ] +SOverlay::Slot() .HAlign(HAlign_Fill) .VAlign(VAlign_Bottom) .Padding(FMargin(24, 0, 24, 0)) [ // Asset discovery indicator AssetDiscoveryIndicator ] ] // Bottom panel +SVerticalBox::Slot() .AutoHeight() [ SNew(SHorizontalBox) // Asset count +SHorizontalBox::Slot() .FillWidth(1.f) .VAlign(VAlign_Center) .Padding(8, 0) [ SNew(STextBlock) .Text(this, &SStructViewer::GetStructCountText) ] // View mode combo button +SHorizontalBox::Slot() .AutoWidth() [ SAssignNew(ViewOptionsComboButton, SComboButton) .ContentPadding(0) .ForegroundColor(this, &SStructViewer::GetViewButtonForegroundColor) .ButtonStyle(FAppStyle::Get(), "ToggleButton") // Use the tool bar item style for this button .OnGetMenuContent(this, &SStructViewer::GetViewButtonContent) .ButtonContent() [ SNew(SHorizontalBox) +SHorizontalBox::Slot() .AutoWidth() .VAlign(VAlign_Center) [ SNew(SImage).Image(FAppStyle::GetBrush("GenericViewButton")) ] +SHorizontalBox::Slot() .AutoWidth() .Padding(2, 0, 0, 0) .VAlign(VAlign_Center) [ SNew(STextBlock).Text(LOCTEXT("ViewButton", "View Options")) ] ] ] ] ] ]; if (ViewOptionsComboButton.IsValid()) { ViewOptionsComboButton->SetVisibility(InitOptions.bAllowViewOptions ? EVisibility::Visible : EVisibility::Collapsed); } // When using a struct picker in list-view mode, the widget will auto-focus the search box // and allow the up and down arrow keys to navigate and enter to pick without using the mouse ever if (InitOptions.Mode == EStructViewerMode::StructPicker && InitOptions.DisplayMode == EStructViewerDisplayMode::ListView) { this->ChildSlot [ SNew(SListViewSelectorDropdownMenu>, SearchBox, StructList) [ StructViewerContent.ToSharedRef() ] ]; } else { this->ChildSlot [ StructViewerContent.ToSharedRef() ]; } // Ensure the struct hierarchy exists, and and watch for changes FStructHierarchy::Get().GetPopulateStructViewerDelegate().AddSP(this, &SStructViewer::Refresh); // Request delayed setting of focus to the search box bPendingFocusNextFrame = true; } END_SLATE_FUNCTION_BUILD_OPTIMIZATION TSharedRef SStructViewer::GetContent() { return SharedThis(this); } SStructViewer::~SStructViewer() { // No longer watching for changes if (FStructHierarchy* StructHierarchy = FStructHierarchy::GetPtr()) { StructHierarchy->GetPopulateStructViewerDelegate().RemoveAll(this); } // Remove the listener for when view settings are changed UStructViewerSettings::OnSettingChanged().RemoveAll(this); } void SStructViewer::ClearSelection() { StructTree->ClearSelection(); } void SStructViewer::OnGetChildrenForStructViewerTree(TSharedPtr InParent, TArray>& OutChildren) { // Simply return the children, it's already setup. OutChildren = InParent->GetChildNodes(); } void SStructViewer::OnStructViewerSelectionChanged(TSharedPtr Item, ESelectInfo::Type SelectInfo) { // Do not act on selection change when it is for navigation if (SelectInfo == ESelectInfo::OnNavigation && InitOptions.DisplayMode == EStructViewerDisplayMode::ListView) { return; } // Sometimes the item is not valid anymore due to filtering. if (Item.IsValid() == false || Item->IsRestricted()) { return; } if (InitOptions.Mode != EStructViewerMode::StructBrowsing) { // Attempt to ensure the struct is loaded Item->LoadStruct(); // Check if the item passed the filter, parent items might be displayed but filtered out and thus not desired to be selected. if (Item->PassedFilter()) { OnStructPicked.ExecuteIfBound(Item->GetStruct()); } else { OnStructPicked.ExecuteIfBound(nullptr); } } } void SStructViewer::OnStructViewerExpansionChanged(TSharedPtr Item, bool bExpanded) { // Sometimes the item is not valid anymore due to filtering. if (!Item.IsValid() || Item->IsRestricted()) { return; } ExpansionStateMap.Add(Item->GetStructPath(), bExpanded); } TSharedPtr SStructViewer::BuildMenuWidget() { TArray> SelectedList; // Based upon which mode the viewer is in, pull the selected item. if (InitOptions.DisplayMode == EStructViewerDisplayMode::TreeView) { SelectedList = StructTree->GetSelectedItems(); } else { SelectedList = StructList->GetSelectedItems(); } // If there is no selected item, return a null widget. if (SelectedList.Num() == 0) { return SNullWidget::NullWidget; } // Get the struct const UScriptStruct* RightClickStruct = SelectedList[0]->GetStruct(); if (bEnableStructDynamicLoading && !RightClickStruct) { SelectedList[0]->LoadStruct(); RightClickStruct = SelectedList[0]->GetStruct(); // Populate the tree/list so any changes to previously unloaded structs will be reflected. Refresh(); } return StructViewer::Helpers::CreateMenu(RightClickStruct); } TSharedRef SStructViewer::OnGenerateRowForStructViewer(TSharedPtr Item, const TSharedRef& OwnerTable) { // If the item was accepted by the filter, leave it bright, otherwise dim it. float AlphaValue = Item->PassedFilter() ? 1.0f : 0.5f; TSharedRef ReturnRow = SNew(SStructItem, OwnerTable) .StructDisplayName(Item->GetStructDisplayName(InitOptions.NameTypeToDisplay)) .HighlightText(SearchBox->GetText()) .TextColor(FLinearColor(1.0f, 1.0f, 1.0f, AlphaValue)) .AssociatedNode(Item) .bIsInStructViewer(InitOptions.Mode == EStructViewerMode::StructBrowsing) .bDynamicStructLoading(bEnableStructDynamicLoading) .OnDragDetected(this, &SStructViewer::OnDragDetected) .OnStructItemDoubleClicked(FOnStructItemDoubleClickDelegate::CreateSP(this, &SStructViewer::ToggleExpansionState_Helper)); // Expand the item if needed. if (!bPendingSetExpansionStates) { bool* bIsExpanded = ExpansionStateMap.Find(Item->GetStructPath()); if (bIsExpanded && *bIsExpanded) { bPendingSetExpansionStates = true; } } return ReturnRow; } const TArray> SStructViewer::GetSelectedItems() const { if (InitOptions.DisplayMode == EStructViewerDisplayMode::ListView) { return StructList->GetSelectedItems(); } return StructTree->GetSelectedItems(); } const int SStructViewer::GetNumItems() const { return NumStructs; } FSlateColor SStructViewer::GetViewButtonForegroundColor() const { static const FName InvertedForegroundName("InvertedForeground"); static const FName DefaultForegroundName("DefaultForeground"); return ViewOptionsComboButton->IsHovered() ? FAppStyle::GetSlateColor(InvertedForegroundName) : FAppStyle::GetSlateColor(DefaultForegroundName); } TSharedRef SStructViewer::GetViewButtonContent() { // Get all menu extenders for this context menu from the content browser module FMenuBuilder MenuBuilder(/*bInShouldCloseWindowAfterMenuSelection=*/true, nullptr, nullptr, /*bCloseSelfOnly=*/ true); MenuBuilder.AddMenuEntry(LOCTEXT("ExpandAll", "Expand All"), LOCTEXT("ExpandAll_Tooltip", "Expands the entire tree"), FSlateIcon(), FUIAction(FExecuteAction::CreateSP(this, &SStructViewer::SetAllExpansionStates, bool(true))), NAME_None, EUserInterfaceActionType::Button); MenuBuilder.AddMenuEntry(LOCTEXT("CollapseAll", "Collapse All"), LOCTEXT("CollapseAll_Tooltip", "Collapses the entire tree"), FSlateIcon(), FUIAction(FExecuteAction::CreateSP(this, &SStructViewer::SetAllExpansionStates, bool(false))), NAME_None, EUserInterfaceActionType::Button); MenuBuilder.BeginSection("Filters", LOCTEXT("StructViewerFiltersHeading", "Struct Filters")); { MenuBuilder.AddMenuEntry( LOCTEXT("ShowInternalStructsOption", "Show Internal Structs"), LOCTEXT("ShowInternalStructsOptionToolTip", "Shows internal-use only structs in the view."), FSlateIcon(), FUIAction( FExecuteAction::CreateSP(this, &SStructViewer::ToggleShowInternalStructs), FCanExecuteAction(), FIsActionChecked::CreateSP(this, &SStructViewer::IsShowingInternalStructs) ), NAME_None, EUserInterfaceActionType::ToggleButton ); } MenuBuilder.EndSection(); MenuBuilder.BeginSection("DeveloperViewType", LOCTEXT("DeveloperViewTypeHeading", "Developer Folder Filter")); { MenuBuilder.AddMenuEntry( LOCTEXT("NoneDeveloperViewOption", "None"), LOCTEXT("NoneDeveloperViewOptionToolTip", "Filter structs to show no structs in developer folders."), FSlateIcon(), FUIAction( FExecuteAction::CreateSP(this, &SStructViewer::SetCurrentDeveloperViewType, EStructViewerDeveloperType::SVDT_None), FCanExecuteAction(), FIsActionChecked::CreateSP(this, &SStructViewer::IsCurrentDeveloperViewType, EStructViewerDeveloperType::SVDT_None) ), NAME_None, EUserInterfaceActionType::RadioButton ); MenuBuilder.AddMenuEntry( LOCTEXT("CurrentUserDeveloperViewOption", "Current Developer"), LOCTEXT("CurrentUserDeveloperViewOptionToolTip", "Filter structs to allow structs in the current user's development folder."), FSlateIcon(), FUIAction( FExecuteAction::CreateSP(this, &SStructViewer::SetCurrentDeveloperViewType, EStructViewerDeveloperType::SVDT_CurrentUser), FCanExecuteAction(), FIsActionChecked::CreateSP(this, &SStructViewer::IsCurrentDeveloperViewType, EStructViewerDeveloperType::SVDT_CurrentUser) ), NAME_None, EUserInterfaceActionType::RadioButton ); MenuBuilder.AddMenuEntry( LOCTEXT("AllUsersDeveloperViewOption", "All Developers"), LOCTEXT("AllUsersDeveloperViewOptionToolTip", "Filter structs to allow structs in all users' development folders."), FSlateIcon(), FUIAction( FExecuteAction::CreateSP(this, &SStructViewer::SetCurrentDeveloperViewType, EStructViewerDeveloperType::SVDT_All), FCanExecuteAction(), FIsActionChecked::CreateSP(this, &SStructViewer::IsCurrentDeveloperViewType, EStructViewerDeveloperType::SVDT_All) ), NAME_None, EUserInterfaceActionType::RadioButton ); } MenuBuilder.EndSection(); return MenuBuilder.MakeWidget(); } void SStructViewer::SetCurrentDeveloperViewType(EStructViewerDeveloperType NewType) { if (ensure((int)NewType < (int)EStructViewerDeveloperType::SVDT_Max) && NewType != GetDefault()->DeveloperFolderType) { GetMutableDefault()->DeveloperFolderType = NewType; GetMutableDefault()->PostEditChange(); } } EStructViewerDeveloperType SStructViewer::GetCurrentDeveloperViewType() const { if (!InitOptions.bAllowViewOptions) { return EStructViewerDeveloperType::SVDT_All; } return GetDefault()->DeveloperFolderType; } bool SStructViewer::IsCurrentDeveloperViewType(EStructViewerDeveloperType ViewType) const { return GetCurrentDeveloperViewType() == ViewType; } void SStructViewer::GetInternalOnlyStructs(TArray>& Structs) { if (!InitOptions.bAllowViewOptions) { return; } Structs = GetDefault()->InternalOnlyStructs; } void SStructViewer::GetInternalOnlyPaths(TArray& Paths) { if (!InitOptions.bAllowViewOptions) { return; } Paths = GetDefault()->InternalOnlyPaths; } FText SStructViewer::GetStructCountText() const { const int32 NumSelectedStructs = GetSelectedItems().Num(); if (NumSelectedStructs == 0) { return FText::Format(LOCTEXT("StructCountLabel", "{0} {0}|plural(one=item,other=items)"), NumStructs); } else { return FText::Format(LOCTEXT("StructCountLabelPlusSelection", "{0} {0}|plural(one=item,other=items) ({1} selected)"), NumStructs, NumSelectedStructs); } } void SStructViewer::ExpandRootNodes() { for (int32 NodeIdx = 0; NodeIdx < RootTreeItems.Num(); ++NodeIdx) { ExpansionStateMap.Add(RootTreeItems[NodeIdx]->GetStructPath(), true); StructTree->SetItemExpansion(RootTreeItems[NodeIdx], true); } } FReply SStructViewer::OnDragDetected(const FGeometry& Geometry, const FPointerEvent& PointerEvent) { if (InitOptions.Mode == EStructViewerMode::StructBrowsing) { const TArray> SelectedItems = GetSelectedItems(); if (SelectedItems.Num() > 0 && SelectedItems[0].IsValid()) { TSharedRef Item = SelectedItems[0].ToSharedRef(); FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked(TEXT("AssetRegistry")); // Spawn a loaded user defined struct just like any other asset from the Content Browser. const FAssetData AssetData = AssetRegistryModule.Get().GetAssetByObjectPath(Item->GetStructPath()); if (AssetData.IsValid()) { return FReply::Handled().BeginDragDrop(FContentBrowserDataDragDropOp::Legacy_New(MakeArrayView(&AssetData, 1))); } } } return FReply::Unhandled(); } void SStructViewer::OnFilterTextChanged(const FText& InFilterText) { // Update the compiled filter and report any syntax error information back to the user TextFilterPtr->SetFilterText(InFilterText); SearchBox->SetError(TextFilterPtr->GetFilterErrorText()); // Repopulate the list to show only what has not been filtered out. Refresh(); } void SStructViewer::OnFilterTextCommitted(const FText& InText, ETextCommit::Type CommitInfo) { if (CommitInfo == ETextCommit::OnEnter) { if (InitOptions.Mode == EStructViewerMode::StructPicker) { TArray> SelectedList = StructList->GetSelectedItems(); if (SelectedList.Num() > 0) { TSharedPtr FirstSelected = SelectedList[0]; const UScriptStruct* Struct = FirstSelected->GetStruct(); // Try and ensure the struct is loaded if (bEnableStructDynamicLoading && !Struct) { FirstSelected->LoadStruct(); Struct = FirstSelected->GetStruct(); } // Check if the item passes the filter, parent items might be displayed but filtered out and thus not desired to be selected. if (Struct && FirstSelected->PassedFilter()) { OnStructPicked.ExecuteIfBound(Struct); } } } } } void SStructViewer::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 < RootTreeItems.Num(); ChildIndex++) { SetAllExpansionStates_Helper(RootTreeItems[ChildIndex], bInExpansionState); } } void SStructViewer::SetAllExpansionStates_Helper(TSharedPtr InNode, bool bInExpansionState) { StructTree->SetItemExpansion(InNode, bInExpansionState); // Recursively go through the children. for (const TSharedPtr& ChildNode : InNode->GetChildNodes()) { SetAllExpansionStates_Helper(ChildNode, bInExpansionState); } } void SStructViewer::ToggleExpansionState_Helper(TSharedPtr InNode) { const bool bExpanded = StructTree->IsItemExpanded(InNode); StructTree->SetItemExpansion(InNode, !bExpanded); } bool SStructViewer::ExpandFilteredInNodes(TSharedPtr InNode) { bool bShouldExpand = InNode->PassedFilter(); for (const TSharedPtr& ChildNode : InNode->GetChildNodes()) { bShouldExpand |= ExpandFilteredInNodes(ChildNode); } if (bShouldExpand) { StructTree->SetItemExpansion(InNode, true); } return bShouldExpand; } void SStructViewer::MapExpansionStatesInTree(TSharedPtr InItem) { ExpansionStateMap.Add(InItem->GetStructPath(), StructTree->IsItemExpanded(InItem)); // Map out all the children, this will be done recursively. for (const TSharedPtr& ChildItem : InItem->GetChildNodes()) { MapExpansionStatesInTree(ChildItem); } } void SStructViewer::SetExpansionStatesInTree(TSharedPtr InItem) { const bool* bIsExpanded = ExpansionStateMap.Find(InItem->GetStructPath()); if (bIsExpanded) { StructTree->SetItemExpansion(InItem, *bIsExpanded); // No reason to set expansion states if the parent is not expanded, it does not seem to do anything. if (*bIsExpanded) { for (const TSharedPtr& ChildItem : InItem->GetChildNodes()) { SetExpansionStatesInTree(ChildItem); } } } else { // Default to no expansion. StructTree->SetItemExpansion(InItem, false); } } int32 SStructViewer::CountTreeItems(FStructViewerNode* Node) { if (Node == nullptr) { return 0; } int32 Count = 1; for (const TSharedPtr& ChildNode : Node->GetChildNodes()) { Count += CountTreeItems(ChildNode.Get()); } return Count; } void SStructViewer::Populate() { bPendingSetExpansionStates = false; // If showing a struct tree, we may need to save expansion states. if (InitOptions.DisplayMode == EStructViewerDisplayMode::TreeView) { if (bSaveExpansionStates) { for (const TSharedPtr& ChildNode : RootTreeItems) { // Check if the item is actually expanded or if it's only expanded because it is root level. bool* bIsExpanded = ExpansionStateMap.Find(ChildNode->GetStructPath()); if ((bIsExpanded && !*bIsExpanded) || !bIsExpanded) { StructTree->SetItemExpansion(ChildNode, false); } // Recursively map out the expansion state of the tree-node. MapExpansionStatesInTree(ChildNode); } } // This is set to false before the call to populate when it is not desired. bSaveExpansionStates = true; } // Empty the tree out so it can be redone. RootTreeItems.Empty(); const bool ShowingInternalStructs = IsShowingInternalStructs(); TArray InternalStructs; TArray InternalPaths; // If we aren't showing the internal structs, then we need to know what structs to consider Internal Only, so let's gather them up from the settings object. if (!ShowingInternalStructs) { TArray> InternalStructNames; GetInternalOnlyPaths(InternalPaths); GetInternalOnlyStructs(InternalStructNames); // Take the package names for the internal only structs and convert them into their UScriptStructs for (const TSoftObjectPtr& InternalStructName : InternalStructNames) { const TSharedPtr StructNode = FStructHierarchy::Get().FindNodeByStructPath(InternalStructName.ToSoftObjectPath()); if (StructNode.IsValid()) { if (const UScriptStruct* Struct = StructNode->GetStruct()) { InternalStructs.Add(Struct); } } } } // Based on if the list or tree is visible we create what will be displayed differently. if (InitOptions.DisplayMode == EStructViewerDisplayMode::TreeView) { // The root node for the tree, will be dummy instance which we will skip. TSharedPtr RootNode; // Get the struct tree, passing in certain filter options. StructViewer::Helpers::GetStructTree(InitOptions, RootNode, *TextFilterPtr, bShowUnloadedStructs, GetCurrentDeveloperViewType(), ShowingInternalStructs, InternalStructs, InternalPaths); // Sort the tree RootNode->SortChildrenRecursive(); // Check if we will restore expansion states, we will not if there is filtering happening. const bool bRestoreExpansionState = TextFilterPtr->GetFilterType() == ETextFilterExpressionType::Empty; // Add all the children of the dummy root. for (const TSharedPtr& ChildItem : RootNode->GetChildNodes()) { RootTreeItems.Add(ChildItem); if (bRestoreExpansionState) { SetExpansionStatesInTree(ChildItem); } // Expand any items that pass the filter. if (TextFilterPtr->GetFilterType() != ETextFilterExpressionType::Empty) { ExpandFilteredInNodes(ChildItem); } } // Only display this option if the user wants it and in Picker Mode. if (InitOptions.bShowNoneOption && InitOptions.Mode == EStructViewerMode::StructPicker) { // @todo - It would seem smart to add this in before the other items, since it needs to be on top. However, that causes strange issues with saving/restoring expansion states. // This is likely not very efficient since the list can have hundreds and even thousands of items. RootTreeItems.Insert(MakeShared(), 0); } NumStructs = 0; for (const TSharedPtr& ChildNode : RootTreeItems) { NumStructs += CountTreeItems(ChildNode.Get()); } // Now that new items are in the tree, we need to request a refresh. StructTree->RequestTreeRefresh(); } else { // Get the struct list, passing in certain filter options. StructViewer::Helpers::GetStructList(InitOptions, RootTreeItems, *TextFilterPtr, bShowUnloadedStructs, GetCurrentDeveloperViewType(), ShowingInternalStructs, InternalStructs, InternalPaths); // Sort the list alphabetically. FStructViewerNode::SortNodes(RootTreeItems, InitOptions.PropertyHandle); // Scroll to selected struct for (const TSharedPtr& Node : RootTreeItems) { if (Node.IsValid()) { if (InitOptions.SelectedStruct == Node->GetStruct()) { StructList->RequestScrollIntoView(Node); } } } // Only display this option if the user wants it and in Picker Mode. if (InitOptions.bShowNoneOption && InitOptions.Mode == EStructViewerMode::StructPicker) { TSharedRef NoneNode = MakeShared(); // @todo - It would seem smart to add this in before the other items, since it needs to be on top. However, that causes strange issues with saving/restoring expansion states. // This is likely not very efficient since the list can have hundreds and even thousands of items. if (StructViewer::Helpers::PassesFilter(NoneNode->GetStructName(), *TextFilterPtr) || StructViewer::Helpers::PassesFilter(NoneNode->GetStructDisplayName().ToString(), *TextFilterPtr)) { RootTreeItems.Insert(MoveTemp(NoneNode), 0); } } NumStructs = 0; for (const TSharedPtr& ChildNode : RootTreeItems) { NumStructs += CountTreeItems(ChildNode.Get()); } // Now that new items are in the list, we need to request a refresh. StructList->RequestListRefresh(); } } FReply SStructViewer::OnKeyDown(const FGeometry& MyGeometry, const FKeyEvent& InKeyEvent) { // Forward key down to struct tree return StructTree->OnKeyDown(MyGeometry, InKeyEvent); } FReply SStructViewer::OnFocusReceived(const FGeometry& MyGeometry, const FFocusEvent& InFocusEvent) { if (RootTreeItems.Num() > 0) { StructTree->SetItemSelection(RootTreeItems[0], true, ESelectInfo::OnMouseClick); StructTree->SetItemExpansion(RootTreeItems[0], true); OnStructViewerSelectionChanged(RootTreeItems[0], ESelectInfo::OnMouseClick); } FSlateApplication::Get().SetKeyboardFocus(SearchBox.ToSharedRef(), EFocusCause::SetDirectly); return FReply::Unhandled(); } bool SStructViewer::SupportsKeyboardFocus() const { return true; } void SStructViewer::DestroyStructHierarchy() { FStructHierarchy::DestroyInstance(); } void SStructViewer::Refresh() { bNeedsRefresh = true; } void SStructViewer::Tick(const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime) { // Will populate the struct hierarchy as needed. FStructHierarchy::Get().UpdateStructHierarchy(); // Move focus to search box if (bPendingFocusNextFrame && SearchBox.IsValid()) { FWidgetPath WidgetToFocusPath; FSlateApplication::Get().GeneratePathToWidgetUnchecked(SearchBox.ToSharedRef(), WidgetToFocusPath); FSlateApplication::Get().SetKeyboardFocus(WidgetToFocusPath, EFocusCause::SetDirectly); bPendingFocusNextFrame = false; } if (bNeedsRefresh) { bNeedsRefresh = false; Populate(); if (InitOptions.bExpandRootNodes) { ExpandRootNodes(); } } if (bPendingSetExpansionStates) { check(RootTreeItems.Num() > 0); SetExpansionStatesInTree(RootTreeItems[0]); bPendingSetExpansionStates = false; } } bool SStructViewer::IsStructAllowed(const UScriptStruct* InStruct) const { return StructViewer::Helpers::IsStructAllowed(InitOptions, InStruct); } void SStructViewer::HandleSettingChanged(FName PropertyName) { if ((PropertyName == "DisplayInternalClasses") || (PropertyName == "DeveloperFolderType") || (PropertyName == NAME_None)) // @todo: Needed if PostEditChange was called manually, for now { Refresh(); } } void SStructViewer::ToggleShowInternalStructs() { check(IsToggleShowInternalStructsAllowed()); GetMutableDefault()->DisplayInternalStructs = !GetDefault()->DisplayInternalStructs; GetMutableDefault()->PostEditChange(); } bool SStructViewer::IsToggleShowInternalStructsAllowed() const { return bCanShowInternalStructs; } bool SStructViewer::IsShowingInternalStructs() const { if (!InitOptions.bAllowViewOptions) { return true; } return IsToggleShowInternalStructsAllowed() && GetDefault()->DisplayInternalStructs; } #undef LOCTEXT_NAMESPACE