Files
UnrealEngineUWP/Engine/Source/Editor/PropertyEditor/Private/SDetailsViewBase.cpp
Andrew Rodham 8ff0d8b98b Added config migration path for newer versions of the engine.
Newly installed versions of the engine will now attempt to copy the project-agnostic config settings from a previous engine installation. This happens by way of a versioned manifest that copies old versions when the manifest does not exist, or is a different version. This code path is benign for non-installed versions of the engine (or FPaths::ShouldSaveToUserDir() is false).

EditorGameAgnosticSettings and EditorUserSettings ini paths have been renamed to EditorSettings and EditorPerProjectUserSettings respectively to better convey their purpose. In general, most settings should be saved in EditorSettings (project-agnostic) so that they apply regardless of which project is open. We have some way to go migrating existing settings for this to be the case, however.

Some previously per-project configuration files are now project-agnostic (such as Editor.ini, EditorKeyBindings.ini, and EditorLayout.ini)

GEditor->Access...Settings and GEditor->Get...Settings have been removed in favor of direct access of the CDO through GetMutableDefault<> and GetDefault<> respectively. Global config ini filenames that are not set up are now neither loaded nor saved on build machines, to handle the problem of indeterminate state more generically.

This addresses UETOOL-270 (Most editor preferences should be project-agnostic)

[CL 2517558 by Andrew Rodham in Main branch]
2015-04-20 10:12:55 -04:00

1200 lines
38 KiB
C++

// Copyright 1998-2015 Epic Games, Inc. All Rights Reserved.
#include "PropertyEditorPrivatePCH.h"
#include "SDetailsViewBase.h"
#include "AssetSelection.h"
#include "PropertyNode.h"
#include "ItemPropertyNode.h"
#include "CategoryPropertyNode.h"
#include "ObjectPropertyNode.h"
#include "ScopedTransaction.h"
#include "AssetThumbnail.h"
#include "SDetailNameArea.h"
#include "IPropertyUtilities.h"
#include "PropertyEditorHelpers.h"
#include "PropertyEditor.h"
#include "PropertyDetailsUtilities.h"
#include "SPropertyEditorEditInline.h"
#include "ObjectEditorUtils.h"
#include "SColorPicker.h"
SDetailsViewBase::~SDetailsViewBase()
{
if (ThumbnailPool.IsValid())
{
ThumbnailPool->ReleaseResources();
}
}
void SDetailsViewBase::OnGetChildrenForDetailTree(TSharedRef<IDetailTreeNode> InTreeNode, TArray< TSharedRef<IDetailTreeNode> >& OutChildren)
{
InTreeNode->GetChildren(OutChildren);
}
TSharedRef<ITableRow> SDetailsViewBase::OnGenerateRowForDetailTree(TSharedRef<IDetailTreeNode> InTreeNode, const TSharedRef<STableViewBase>& OwnerTable)
{
return InTreeNode->GenerateNodeWidget(OwnerTable, ColumnSizeData, PropertyUtilities.ToSharedRef());
}
void SDetailsViewBase::SetRootExpansionStates(const bool bExpand, const bool bRecurse)
{
for (auto Iter = RootTreeNodes.CreateIterator(); Iter; ++Iter)
{
SetNodeExpansionState(*Iter, bExpand, bRecurse);
}
}
void SDetailsViewBase::SetNodeExpansionState(TSharedRef<IDetailTreeNode> InTreeNode, bool bIsItemExpanded, bool bRecursive)
{
TArray< TSharedRef<IDetailTreeNode> > Children;
InTreeNode->GetChildren(Children);
if (Children.Num())
{
RequestItemExpanded(InTreeNode, bIsItemExpanded);
InTreeNode->OnItemExpansionChanged(bIsItemExpanded);
if (bRecursive)
{
for (int32 ChildIndex = 0; ChildIndex < Children.Num(); ++ChildIndex)
{
TSharedRef<IDetailTreeNode> Child = Children[ChildIndex];
SetNodeExpansionState(Child, bIsItemExpanded, bRecursive);
}
}
}
}
void SDetailsViewBase::SetNodeExpansionStateRecursive(TSharedRef<IDetailTreeNode> InTreeNode, bool bIsItemExpanded)
{
SetNodeExpansionState(InTreeNode, bIsItemExpanded, true);
}
void SDetailsViewBase::OnItemExpansionChanged(TSharedRef<IDetailTreeNode> InTreeNode, bool bIsItemExpanded)
{
SetNodeExpansionState(InTreeNode, bIsItemExpanded, false);
}
FReply SDetailsViewBase::OnLockButtonClicked()
{
bIsLocked = !bIsLocked;
return FReply::Handled();
}
void SDetailsViewBase::HideFilterArea(bool bHide)
{
DetailsViewArgs.bAllowSearch = !bHide;
}
static void GetPropertiesInOrderDisplayedRecursive(const TArray< TSharedRef<IDetailTreeNode> >& TreeNodes, TArray< FPropertyPath > &OutLeaves)
{
for (auto& TreeNode : TreeNodes)
{
if (TreeNode->IsLeaf())
{
FPropertyPath Path = TreeNode->GetPropertyPath();
// Some leaf nodes are not associated with properties, specifically the collision presets.
// @todo doc: investigate what we can do about this, result is that for these fields
// we can't highlight hte property in the diff tool.
if( Path.GetNumProperties() != 0 )
{
OutLeaves.Push(Path);
}
}
else
{
TArray< TSharedRef<IDetailTreeNode> > Children;
TreeNode->GetChildren(Children);
GetPropertiesInOrderDisplayedRecursive(Children, OutLeaves);
}
}
}
TArray< FPropertyPath > SDetailsViewBase::GetPropertiesInOrderDisplayed() const
{
TArray< FPropertyPath > Ret;
GetPropertiesInOrderDisplayedRecursive(RootTreeNodes, Ret);
return Ret;
}
static TSharedPtr< IDetailTreeNode > FindTreeNodeFromPropertyRecursive( const TArray< TSharedRef<IDetailTreeNode> >& Nodes, const FPropertyPath& Property )
{
for (auto& TreeNode : Nodes)
{
if (TreeNode->IsLeaf())
{
FPropertyPath tmp = TreeNode->GetPropertyPath();
if( Property == tmp )
{
return TreeNode;
}
}
else
{
TArray< TSharedRef<IDetailTreeNode> > Children;
TreeNode->GetChildren(Children);
auto Result = FindTreeNodeFromPropertyRecursive(Children, Property);
if( Result.IsValid() )
{
return Result;
}
}
}
return TSharedPtr< IDetailTreeNode >();
}
void SDetailsViewBase::HighlightProperty(const FPropertyPath& Property)
{
auto PrevHighlightedNodePtr = CurrentlyHighlightedNode.Pin();
if (PrevHighlightedNodePtr.IsValid())
{
PrevHighlightedNodePtr->SetIsHighlighted(false);
}
auto NextNodePtr = FindTreeNodeFromPropertyRecursive( RootTreeNodes, Property );
if (NextNodePtr.IsValid())
{
NextNodePtr->SetIsHighlighted(true);
auto ParentCategory = NextNodePtr->GetParentCategory();
if (ParentCategory.IsValid())
{
DetailTree->SetItemExpansion(ParentCategory.ToSharedRef(), true);
}
DetailTree->RequestScrollIntoView(NextNodePtr.ToSharedRef());
}
CurrentlyHighlightedNode = NextNodePtr;
}
void SDetailsViewBase::ShowAllAdvancedProperties()
{
CurrentFilter.bShowAllAdvanced = true;
}
void SDetailsViewBase::SetOnDisplayedPropertiesChanged(FOnDisplayedPropertiesChanged InOnDisplayedPropertiesChangedDelegate)
{
OnDisplayedPropertiesChangedDelegate = InOnDisplayedPropertiesChangedDelegate;
}
EVisibility SDetailsViewBase::GetTreeVisibility() const
{
return DetailLayout.IsValid() && DetailLayout->HasDetails() ? EVisibility::Visible : EVisibility::Collapsed;
}
/** Returns the image used for the icon on the filter button */
const FSlateBrush* SDetailsViewBase::OnGetFilterButtonImageResource() const
{
if (bHasActiveFilter)
{
return FEditorStyle::GetBrush(TEXT("PropertyWindow.FilterCancel"));
}
else
{
return FEditorStyle::GetBrush(TEXT("PropertyWindow.FilterSearch"));
}
}
void SDetailsViewBase::EnqueueDeferredAction(FSimpleDelegate& DeferredAction)
{
DeferredActions.Add(DeferredAction);
}
/**
* Creates the color picker window for this property view.
*
* @param Node The slate property node to edit.
* @param bUseAlpha Whether or not alpha is supported
*/
void SDetailsViewBase::CreateColorPickerWindow(const TSharedRef< FPropertyEditor >& PropertyEditor, bool bUseAlpha)
{
const TSharedRef< FPropertyNode > PinnedColorPropertyNode = PropertyEditor->GetPropertyNode();
ColorPropertyNode = PinnedColorPropertyNode;
UProperty* Property = PinnedColorPropertyNode->GetProperty();
check(Property);
FReadAddressList ReadAddresses;
PinnedColorPropertyNode->GetReadAddress(false, ReadAddresses, false);
TArray<FLinearColor*> LinearColor;
TArray<FColor*> DWORDColor;
for (int32 ColorIndex = 0; ColorIndex < ReadAddresses.Num(); ++ColorIndex)
{
const uint8* Addr = ReadAddresses.GetAddress(ColorIndex);
if (Addr)
{
if (Cast<UStructProperty>(Property)->Struct->GetFName() == NAME_Color)
{
DWORDColor.Add((FColor*)Addr);
}
else
{
check(Cast<UStructProperty>(Property)->Struct->GetFName() == NAME_LinearColor);
LinearColor.Add((FLinearColor*)Addr);
}
}
}
bHasOpenColorPicker = true;
FColorPickerArgs PickerArgs;
PickerArgs.ParentWidget = AsShared();
PickerArgs.bUseAlpha = bUseAlpha;
PickerArgs.DisplayGamma = TAttribute<float>::Create(TAttribute<float>::FGetter::CreateUObject(GEngine, &UEngine::GetDisplayGamma));
PickerArgs.ColorArray = &DWORDColor;
PickerArgs.LinearColorArray = &LinearColor;
PickerArgs.OnColorCommitted = FOnLinearColorValueChanged::CreateSP(this, &SDetailsViewBase::SetColorPropertyFromColorPicker);
PickerArgs.OnColorPickerWindowClosed = FOnWindowClosed::CreateSP(this, &SDetailsViewBase::OnColorPickerWindowClosed);
OpenColorPicker(PickerArgs);
}
void SDetailsViewBase::SetColorPropertyFromColorPicker(FLinearColor NewColor)
{
const TSharedPtr< FPropertyNode > PinnedColorPropertyNode = ColorPropertyNode.Pin();
if (ensure(PinnedColorPropertyNode.IsValid()))
{
UProperty* Property = PinnedColorPropertyNode->GetProperty();
check(Property);
FObjectPropertyNode* ObjectNode = PinnedColorPropertyNode->FindObjectItemParent();
if (ObjectNode && ObjectNode->GetNumObjects())
{
FScopedTransaction Transaction(NSLOCTEXT("UnrealEd", "SetColorProperty", "Set Color Property"));
PinnedColorPropertyNode->NotifyPreChange(Property, GetNotifyHook());
FPropertyChangedEvent ChangeEvent(Property, EPropertyChangeType::ValueSet);
PinnedColorPropertyNode->NotifyPostChange(ChangeEvent, GetNotifyHook());
}
}
}
void SDetailsViewBase::OnColorPickerWindowClosed(const TSharedRef<SWindow>& Window)
{
const TSharedPtr< FPropertyNode > PinnedColorPropertyNode = ColorPropertyNode.Pin();
if (ensure(PinnedColorPropertyNode.IsValid()))
{
UProperty* Property = PinnedColorPropertyNode->GetProperty();
if (Property && PropertyUtilities.IsValid())
{
FPropertyChangedEvent ChangeEvent(Property, EPropertyChangeType::ArrayAdd);
PinnedColorPropertyNode->FixPropertiesInEvent(ChangeEvent);
PropertyUtilities->NotifyFinishedChangingProperties(ChangeEvent);
}
}
// A color picker window is no longer open
bHasOpenColorPicker = false;
ColorPropertyNode.Reset();
}
void SDetailsViewBase::SetIsPropertyVisibleDelegate(FIsPropertyVisible InIsPropertyVisible)
{
IsPropertyVisibleDelegate = InIsPropertyVisible;
}
void SDetailsViewBase::SetIsPropertyReadOnlyDelegate(FIsPropertyReadOnly InIsPropertyReadOnly)
{
IsPropertyReadOnlyDelegate = InIsPropertyReadOnly;
}
void SDetailsViewBase::SetIsPropertyEditingEnabledDelegate(FIsPropertyEditingEnabled IsPropertyEditingEnabled)
{
IsPropertyEditingEnabledDelegate = IsPropertyEditingEnabled;
}
bool SDetailsViewBase::IsPropertyEditingEnabled() const
{
// If the delegate is not bound assume property editing is enabled, otherwise ask the delegate
return !IsPropertyEditingEnabledDelegate.IsBound() || IsPropertyEditingEnabledDelegate.Execute();
}
void SDetailsViewBase::SetKeyframeHandler( TSharedPtr<class IDetailKeyframeHandler> InKeyframeHandler )
{
KeyframeHandler = InKeyframeHandler;
}
TSharedPtr<IDetailKeyframeHandler> SDetailsViewBase::GetKeyframeHandler()
{
return KeyframeHandler;
}
void SDetailsViewBase::SetExtensionHandler(TSharedPtr<class IDetailPropertyExtensionHandler> InExtensionHandler)
{
ExtensionHandler = InExtensionHandler;
}
TSharedPtr<IDetailPropertyExtensionHandler> SDetailsViewBase::GetExtensionHandler()
{
return ExtensionHandler;
}
void SDetailsViewBase::SetGenericLayoutDetailsDelegate(FOnGetDetailCustomizationInstance OnGetGenericDetails)
{
GenericLayoutDelegate = OnGetGenericDetails;
}
TSharedPtr<FAssetThumbnailPool> SDetailsViewBase::GetThumbnailPool() const
{
if (!ThumbnailPool.IsValid())
{
// Create a thumbnail pool for the view if it doesnt exist. This does not use resources of no thumbnails are used
ThumbnailPool = MakeShareable(new FAssetThumbnailPool(50, TAttribute<bool>::Create(TAttribute<bool>::FGetter::CreateSP(this, &SDetailsView::IsHovered))));
}
return ThumbnailPool;
}
void SDetailsViewBase::NotifyFinishedChangingProperties(const FPropertyChangedEvent& PropertyChangedEvent)
{
OnFinishedChangingPropertiesDelegate.Broadcast(PropertyChangedEvent);
}
void SDetailsViewBase::RequestItemExpanded(TSharedRef<IDetailTreeNode> TreeNode, bool bExpand)
{
// Don't change expansion state if its already in that state
if (DetailTree->IsItemExpanded(TreeNode) != bExpand)
{
FilteredNodesRequestingExpansionState.Add(TreeNode, bExpand);
}
}
void SDetailsViewBase::RefreshTree()
{
if (OnDisplayedPropertiesChangedDelegate.IsBound())
{
OnDisplayedPropertiesChangedDelegate.Execute();
}
DetailTree->RequestTreeRefresh();
}
void SDetailsViewBase::SaveCustomExpansionState(const FString& NodePath, bool bIsExpanded)
{
if (bIsExpanded)
{
ExpandedDetailNodes.Add(NodePath);
}
else
{
ExpandedDetailNodes.Remove(NodePath);
}
}
bool SDetailsViewBase::GetCustomSavedExpansionState(const FString& NodePath) const
{
return ExpandedDetailNodes.Contains(NodePath);
}
bool SDetailsViewBase::IsPropertyVisible( const FPropertyAndParent& PropertyAndParent ) const
{
return IsPropertyVisibleDelegate.IsBound() ? IsPropertyVisibleDelegate.Execute(PropertyAndParent) : true;
}
bool SDetailsViewBase::IsPropertyReadOnly( const FPropertyAndParent& PropertyAndParent ) const
{
return IsPropertyReadOnlyDelegate.IsBound() ? IsPropertyReadOnlyDelegate.Execute(PropertyAndParent) : false;
}
TSharedPtr<IPropertyUtilities> SDetailsViewBase::GetPropertyUtilities()
{
return PropertyUtilities;
}
void SDetailsViewBase::OnShowOnlyModifiedClicked()
{
CurrentFilter.bShowOnlyModifiedProperties = !CurrentFilter.bShowOnlyModifiedProperties;
UpdateFilteredDetails();
}
void SDetailsViewBase::OnShowAllAdvancedClicked()
{
CurrentFilter.bShowAllAdvanced = !CurrentFilter.bShowAllAdvanced;
UpdateFilteredDetails();
}
void SDetailsViewBase::OnShowOnlyDifferingClicked()
{
CurrentFilter.bShowOnlyDiffering = !CurrentFilter.bShowOnlyDiffering;
UpdateFilteredDetails();
}
void SDetailsViewBase::OnShowAllChildrenIfCategoryMatchesClicked()
{
CurrentFilter.bShowAllChildrenIfCategoryMatches = !CurrentFilter.bShowAllChildrenIfCategoryMatches;
UpdateFilteredDetails();
}
/** Called when the filter text changes. This filters specific property nodes out of view */
void SDetailsViewBase::OnFilterTextChanged(const FText& InFilterText)
{
FString InFilterString = InFilterText.ToString();
InFilterString.Trim().TrimTrailing();
// Was the filter just cleared
bool bFilterCleared = InFilterString.Len() == 0 && CurrentFilter.FilterStrings.Num() > 0;
FilterView(InFilterString);
}
TSharedPtr<SWidget> SDetailsViewBase::GetNameAreaWidget()
{
return DetailsViewArgs.bCustomNameAreaLocation ? NameArea : nullptr;
}
TSharedPtr<SWidget> SDetailsViewBase::GetFilterAreaWidget()
{
return DetailsViewArgs.bCustomFilterAreaLocation ? FilterRow : nullptr;
}
TSharedPtr<class FUICommandList> SDetailsViewBase::GetHostCommandList() const
{
return DetailsViewArgs.HostCommandList;
}
/**
* Hides or shows properties based on the passed in filter text
*
* @param InFilterText The filter text
*/
void SDetailsViewBase::FilterView(const FString& InFilterText)
{
TArray<FString> CurrentFilterStrings;
FString ParseString = InFilterText;
// Remove whitespace from the front and back of the string
ParseString.Trim();
ParseString.TrimTrailing();
ParseString.ParseIntoArray(CurrentFilterStrings, TEXT(" "), true);
bHasActiveFilter = CurrentFilterStrings.Num() > 0;
CurrentFilter.FilterStrings = CurrentFilterStrings;
UpdateFilteredDetails();
}
void SDetailsViewBase::QueryLayoutForClass(FDetailLayoutBuilderImpl& CustomDetailLayout, UStruct* Class)
{
CustomDetailLayout.SetCurrentCustomizationClass(CastChecked<UClass>(Class), NAME_None);
FPropertyEditorModule& ParentPlugin = FModuleManager::GetModuleChecked<FPropertyEditorModule>("PropertyEditor");
FCustomDetailLayoutNameMap& GlobalCustomLayoutNameMap = ParentPlugin.ClassNameToDetailLayoutNameMap;
// Check the instanced map first
FDetailLayoutCallback* Callback = InstancedClassToDetailLayoutMap.Find(TWeakObjectPtr<UStruct>(Class));
if (!Callback)
{
// callback wasn't found in the per instance map, try the global instances instead
Callback = GlobalCustomLayoutNameMap.Find(Class->GetFName());
}
if (Callback && Callback->DetailLayoutDelegate.IsBound())
{
// Create a new instance of the custom detail layout for the current class
TSharedRef<IDetailCustomization> CustomizationInstance = Callback->DetailLayoutDelegate.Execute();
// Ask for details immediately
CustomizationInstance->CustomizeDetails(CustomDetailLayout);
// Save the instance from destruction until we refresh
CustomizationClassInstances.Add(CustomizationInstance);
}
}
void SDetailsViewBase::QueryCustomDetailLayout(FDetailLayoutBuilderImpl& CustomDetailLayout)
{
FPropertyEditorModule& ParentPlugin = FModuleManager::GetModuleChecked<FPropertyEditorModule>("PropertyEditor");
// Get the registered classes that customize details
FCustomDetailLayoutNameMap& GlobalCustomLayoutNameMap = ParentPlugin.ClassNameToDetailLayoutNameMap;
UStruct* BaseStruct = GetBaseStruct();
// All the current customization instances need to be deleted when it is safe
CustomizationClassInstancesPendingDelete = CustomizationClassInstances;
CustomizationClassInstances.Empty();
//Ask for generic details not specific to an object being viewed
if (GenericLayoutDelegate.IsBound())
{
// Create a new instance of the custom detail layout for the current class
TSharedRef<IDetailCustomization> CustomizationInstance = GenericLayoutDelegate.Execute();
// Ask for details immediately
CustomizationInstance->CustomizeDetails(CustomDetailLayout);
// Save the instance from destruction until we refresh
CustomizationClassInstances.Add(CustomizationInstance);
}
// Sort them by query order. @todo not good enough
struct FCompareFDetailLayoutCallback
{
FORCEINLINE bool operator()(const FDetailLayoutCallback& A, const FDetailLayoutCallback& B) const
{
return A.Order < B.Order;
}
};
TMap< TWeakObjectPtr<UStruct>, FDetailLayoutCallback*> FinalCallbackMap;
for (auto ClassIt = ClassesWithProperties.CreateConstIterator(); ClassIt; ++ClassIt)
{
// Check the instanced map first
FDetailLayoutCallback* Callback = InstancedClassToDetailLayoutMap.Find(*ClassIt);
if (!Callback)
{
// callback wasn't found in the per instance map, try the global instances instead
Callback = GlobalCustomLayoutNameMap.Find((*ClassIt)->GetFName());
}
if (Callback)
{
FinalCallbackMap.Add(*ClassIt, Callback);
}
}
FinalCallbackMap.ValueSort(FCompareFDetailLayoutCallback());
TSet<UStruct*> QueriedClasses;
if (FinalCallbackMap.Num() > 0)
{
// Ask each class that we have properties for to customize its layout
for (auto LayoutIt(FinalCallbackMap.CreateConstIterator()); LayoutIt; ++LayoutIt)
{
const TWeakObjectPtr<UStruct> WeakClass = LayoutIt.Key();
if (WeakClass.IsValid())
{
UStruct* Class = WeakClass.Get();
FClassInstanceToPropertyMap& InstancedPropertyMap = ClassToPropertyMap.FindChecked(Class->GetFName());
for (FClassInstanceToPropertyMap::TIterator InstanceIt(InstancedPropertyMap); InstanceIt; ++InstanceIt)
{
FName Key = InstanceIt.Key();
CustomDetailLayout.SetCurrentCustomizationClass(CastChecked<UClass>(Class), Key);
const FOnGetDetailCustomizationInstance& DetailDelegate = LayoutIt.Value()->DetailLayoutDelegate;
if (DetailDelegate.IsBound())
{
QueriedClasses.Add(Class);
// Create a new instance of the custom detail layout for the current class
TSharedRef<IDetailCustomization> CustomizationInstance = DetailDelegate.Execute();
// Ask for details immediately
CustomizationInstance->CustomizeDetails(CustomDetailLayout);
// Save the instance from destruction until we refresh
CustomizationClassInstances.Add(CustomizationInstance);
}
}
}
}
}
// Ensure that the base class and its parents are always queried
TSet<UStruct*> ParentClassesToQuery;
if (BaseStruct && !QueriedClasses.Contains(BaseStruct))
{
ParentClassesToQuery.Add(BaseStruct);
ClassesWithProperties.Add(BaseStruct);
}
// Find base classes of queried classes that were not queried and add them to the query list
// this supports cases where a parent class has no properties but still wants to add customization
for (auto QueriedClassIt = ClassesWithProperties.CreateConstIterator(); QueriedClassIt; ++QueriedClassIt)
{
UStruct* ParentStruct = (*QueriedClassIt)->GetSuperStruct();
while (ParentStruct && ParentStruct->IsA(UClass::StaticClass()) && !QueriedClasses.Contains(ParentStruct) && !ClassesWithProperties.Contains(ParentStruct))
{
ParentClassesToQuery.Add(ParentStruct);
ParentStruct = ParentStruct->GetSuperStruct();
}
}
// Query extra base classes
for (auto ParentIt = ParentClassesToQuery.CreateConstIterator(); ParentIt; ++ParentIt)
{
if (Cast<UClass>(*ParentIt))
{
QueryLayoutForClass(CustomDetailLayout, *ParentIt);
}
}
}
EVisibility SDetailsViewBase::GetFilterBoxVisibility() const
{
// Visible if we allow search and we have anything to search otherwise collapsed so it doesn't take up room
return (DetailsViewArgs.bAllowSearch && IsConnected()) ? EVisibility::Visible : EVisibility::Collapsed;
}
bool SDetailsViewBase::SupportsKeyboardFocus() const
{
return DetailsViewArgs.bSearchInitialKeyFocus && SearchBox->SupportsKeyboardFocus() && GetFilterBoxVisibility() == EVisibility::Visible;
}
FReply SDetailsViewBase::OnFocusReceived(const FGeometry& MyGeometry, const FFocusEvent& InFocusEvent)
{
FReply Reply = FReply::Handled();
if (InFocusEvent.GetCause() != EFocusCause::Cleared)
{
Reply.SetUserFocus(SearchBox.ToSharedRef(), InFocusEvent.GetCause());
}
return Reply;
}
/** Ticks the property view. This function performs a data consistency check */
void SDetailsViewBase::Tick( const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime )
{
for (int32 i = 0; i < CustomizationClassInstancesPendingDelete.Num(); ++i)
{
ensure(CustomizationClassInstancesPendingDelete[i].IsUnique());
}
if (RootNodePendingKill.IsValid())
{
RootNodePendingKill->Disconnect();
RootNodePendingKill.Reset();
}
// Empty all the customization instances that need to be deleted
CustomizationClassInstancesPendingDelete.Empty();
auto RootPropertyNode = GetRootNode();
check(RootPropertyNode.IsValid());
// Purge any objects that are marked pending kill from the object list
if (auto ObjectRoot = RootPropertyNode->AsObjectNode())
{
ObjectRoot->PurgeKilledObjects();
}
if (DeferredActions.Num() > 0)
{
// Any deferred actions are likely to cause the node tree to be at least partially rebuilt
// Save the expansion state of existing nodes so we can expand them later
SaveExpandedItems();
// Execute any deferred actions
for (int32 ActionIndex = 0; ActionIndex < DeferredActions.Num(); ++ActionIndex)
{
DeferredActions[ActionIndex].ExecuteIfBound();
}
DeferredActions.Empty();
}
if( RootPropertyNode == RootNodePendingKill )
{
// Reaquire the root property node. It may have been changed by the deferred actions if something like a blueprint editor forcefully resets a details panel during a posteditchange
RootPropertyNode = GetRootNode();
RestoreExpandedItems();
}
bool bValidateExternalNodes = true;
FPropertyNode::DataValidationResult Result = RootPropertyNode->EnsureDataIsValid();
if (Result == FPropertyNode::PropertiesChanged || Result == FPropertyNode::EditInlineNewValueChanged)
{
RestoreExpandedItems();
UpdatePropertyMap();
}
else if (Result == FPropertyNode::ArraySizeChanged)
{
RestoreExpandedItems();
UpdateFilteredDetails();
}
else if (Result == FPropertyNode::ObjectInvalid)
{
ForceRefresh();
// All objects are being reset, no need to validate external nodes
bValidateExternalNodes = false;
}
if (bValidateExternalNodes)
{
for (int32 NodeIndex = 0; NodeIndex < ExternalRootPropertyNodes.Num(); ++NodeIndex)
{
TSharedPtr<FPropertyNode> PropertyNode = ExternalRootPropertyNodes[NodeIndex].Pin();
if (PropertyNode.IsValid())
{
Result = PropertyNode->EnsureDataIsValid();
if (Result == FPropertyNode::PropertiesChanged || Result == FPropertyNode::EditInlineNewValueChanged)
{
RestoreExpandedItems(PropertyNode);
UpdatePropertyMap();
// Note this will invalidate all the external root nodes so there is no need to continue
ExternalRootPropertyNodes.Empty();
break;
}
else if (Result == FPropertyNode::ArraySizeChanged)
{
RestoreExpandedItems(PropertyNode);
UpdateFilteredDetails();
}
}
else
{
// Remove the current node if it is no longer valid
ExternalRootPropertyNodes.RemoveAt(NodeIndex);
--NodeIndex;
}
}
}
if (DetailLayout.IsValid())
{
DetailLayout->Tick(InDeltaTime);
}
if (!ColorPropertyNode.IsValid() && bHasOpenColorPicker)
{
// Destroy the color picker window if the color property node has become invalid
DestroyColorPicker();
bHasOpenColorPicker = false;
}
if (FilteredNodesRequestingExpansionState.Num() > 0)
{
// change expansion state on the nodes that request it
for (TMap<TSharedRef<IDetailTreeNode>, bool >::TConstIterator It(FilteredNodesRequestingExpansionState); It; ++It)
{
DetailTree->SetItemExpansion(It.Key(), It.Value());
}
FilteredNodesRequestingExpansionState.Empty();
}
}
/**
* Recursively gets expanded items for a node
*
* @param InPropertyNode The node to get expanded items from
* @param OutExpandedItems List of expanded items that were found
*/
void GetExpandedItems(TSharedPtr<FPropertyNode> InPropertyNode, TArray<FString>& OutExpandedItems)
{
if (InPropertyNode->HasNodeFlags(EPropertyNodeFlags::Expanded))
{
const bool bWithArrayIndex = true;
FString Path;
Path.Empty(128);
InPropertyNode->GetQualifiedName(Path, bWithArrayIndex);
OutExpandedItems.Add(Path);
}
for (int32 ChildIndex = 0; ChildIndex < InPropertyNode->GetNumChildNodes(); ++ChildIndex)
{
GetExpandedItems(InPropertyNode->GetChildNode(ChildIndex), OutExpandedItems);
}
}
/**
* Recursively sets expanded items for a node
*
* @param InNode The node to set expanded items on
* @param OutExpandedItems List of expanded items to set
*/
void SetExpandedItems(TSharedPtr<FPropertyNode> InPropertyNode, const TArray<FString>& InExpandedItems)
{
if (InExpandedItems.Num() > 0)
{
const bool bWithArrayIndex = true;
FString Path;
Path.Empty(128);
InPropertyNode->GetQualifiedName(Path, bWithArrayIndex);
for (int32 ItemIndex = 0; ItemIndex < InExpandedItems.Num(); ++ItemIndex)
{
if (InExpandedItems[ItemIndex] == Path)
{
InPropertyNode->SetNodeFlags(EPropertyNodeFlags::Expanded, true);
break;
}
}
for (int32 NodeIndex = 0; NodeIndex < InPropertyNode->GetNumChildNodes(); ++NodeIndex)
{
SetExpandedItems(InPropertyNode->GetChildNode(NodeIndex), InExpandedItems);
}
}
}
void SDetailsViewBase::SaveExpandedItems()
{
auto RootPropertyNode = GetRootNode();
check(RootPropertyNode.IsValid());
UStruct* BestBaseStruct = RootPropertyNode->GetBaseStructure();
TArray<FString> ExpandedPropertyItems;
GetExpandedItems(RootPropertyNode, ExpandedPropertyItems);
// Handle spaces in expanded node names by wrapping them in quotes
for( FString& String : ExpandedPropertyItems )
{
String.InsertAt(0, '"');
String.AppendChar('"');
}
TArray<FString> ExpandedCustomItems = ExpandedDetailNodes.Array();
// Expanded custom items may have spaces but SetSingleLineArray doesnt support spaces (treats it as another element in the array)
// Append a '|' after each element instead
FString ExpandedCustomItemsString;
for (auto It = ExpandedDetailNodes.CreateConstIterator(); It; ++It)
{
ExpandedCustomItemsString += *It;
ExpandedCustomItemsString += TEXT(",");
}
//while a valid class, and we're either the same as the base class (for multiple actors being selected and base class is AActor) OR we're not down to AActor yet)
for (UStruct* Struct = BestBaseStruct; Struct && ((BestBaseStruct == Struct) || (Struct != AActor::StaticClass())); Struct = Struct->GetSuperStruct())
{
if (RootPropertyNode->GetNumChildNodes() > 0)
{
bool bShouldSave = ExpandedPropertyItems.Num() > 0;
if (!bShouldSave)
{
TArray<FString> DummyExpandedPropertyItems;
GConfig->GetSingleLineArray(TEXT("DetailPropertyExpansion"), *Struct->GetName(), DummyExpandedPropertyItems, GEditorPerProjectIni);
bShouldSave = DummyExpandedPropertyItems.Num() > 0;
}
if (bShouldSave)
{
GConfig->SetSingleLineArray(TEXT("DetailPropertyExpansion"), *Struct->GetName(), ExpandedPropertyItems, GEditorPerProjectIni);
}
}
}
if (DetailLayout.IsValid() && BestBaseStruct)
{
bool bShouldSave = !ExpandedCustomItemsString.IsEmpty();
if (!bShouldSave)
{
FString DummyExpandedCustomItemsString;
GConfig->GetString(TEXT("DetailCustomWidgetExpansion"), *BestBaseStruct->GetName(), DummyExpandedCustomItemsString, GEditorPerProjectIni);
bShouldSave = !DummyExpandedCustomItemsString.IsEmpty();
}
if (bShouldSave)
{
GConfig->SetString(TEXT("DetailCustomWidgetExpansion"), *BestBaseStruct->GetName(), *ExpandedCustomItemsString, GEditorPerProjectIni);
}
}
}
void SDetailsViewBase::RestoreExpandedItems(TSharedPtr<FPropertyNode> InitialStartNode)
{
auto RootPropertyNode = GetRootNode();
check(RootPropertyNode.IsValid());
TSharedPtr<FPropertyNode> StartNode = InitialStartNode;
if (!StartNode.IsValid())
{
StartNode = RootPropertyNode;
}
ExpandedDetailNodes.Empty();
TArray<FString> ExpandedPropertyItems;
FString ExpandedCustomItems;
UStruct* BestBaseStruct = RootPropertyNode->GetBaseStructure();
//while a valid class, and we're either the same as the base class (for multiple actors being selected and base class is AActor) OR we're not down to AActor yet)
for (UStruct* Struct = BestBaseStruct; Struct && ((BestBaseStruct == Struct) || (Struct != AActor::StaticClass())); Struct = Struct->GetSuperStruct())
{
GConfig->GetSingleLineArray(TEXT("DetailPropertyExpansion"), *Struct->GetName(), ExpandedPropertyItems, GEditorPerProjectIni);
SetExpandedItems(StartNode, ExpandedPropertyItems);
}
if (BestBaseStruct)
{
GConfig->GetString(TEXT("DetailCustomWidgetExpansion"), *BestBaseStruct->GetName(), ExpandedCustomItems, GEditorPerProjectIni);
TArray<FString> ExpandedCustomItemsArray;
ExpandedCustomItems.ParseIntoArray(ExpandedCustomItemsArray, TEXT(","), true);
ExpandedDetailNodes.Append(ExpandedCustomItemsArray);
}
}
void SDetailsViewBase::UpdateFilteredDetails()
{
auto RootPropertyNode = GetRootNode();
if (RootPropertyNode.IsValid())
{
RootPropertyNode->FilterNodes(CurrentFilter.FilterStrings);
RootPropertyNode->ProcessSeenFlags(true);
for (int32 NodeIndex = 0; NodeIndex < ExternalRootPropertyNodes.Num(); ++NodeIndex)
{
TSharedPtr<FPropertyNode> PropertyNode = ExternalRootPropertyNodes[NodeIndex].Pin();
if (PropertyNode.IsValid())
{
PropertyNode->FilterNodes(CurrentFilter.FilterStrings);
PropertyNode->ProcessSeenFlags(true);
}
}
if (DetailLayout.IsValid())
{
DetailLayout->FilterDetailLayout(CurrentFilter);
}
RootTreeNodes = DetailLayout->GetRootTreeNodes();
}
RefreshTree();
}
/**
* Determines whether or not a property should be visible in the default generated detail layout
*
* @param PropertyNode The property node to check
* @param ParentNode The parent property node to check
* @return true if the property should be visible
*/
static bool IsVisibleStandaloneProperty(const FPropertyNode& PropertyNode, const FPropertyNode& ParentNode)
{
const UProperty* Property = PropertyNode.GetProperty();
const UArrayProperty* ParentArrayProperty = Cast<const UArrayProperty>(ParentNode.GetProperty());
bool bIsVisibleStandalone = false;
if (Property)
{
if (Property->IsA(UObjectPropertyBase::StaticClass()))
{
// Do not add this child node to the current map if its a single object property in a category (serves no purpose for UI)
bIsVisibleStandalone = !ParentArrayProperty && (PropertyNode.GetNumChildNodes() == 0 || PropertyNode.GetNumChildNodes() > 1);
}
else if (Property->IsA(UArrayProperty::StaticClass()) || (Property->ArrayDim > 1 && PropertyNode.GetArrayIndex() == INDEX_NONE))
{
// Base array properties are always visible
bIsVisibleStandalone = true;
}
else
{
bIsVisibleStandalone = true;
}
}
return bIsVisibleStandalone;
}
void SDetailsViewBase::UpdatePropertyMapRecursive(FPropertyNode& InNode, FDetailLayoutBuilderImpl& InDetailLayout, FName CurCategory, FComplexPropertyNode* CurObjectNode)
{
UProperty* ParentProperty = InNode.GetProperty();
UStructProperty* ParentStructProp = Cast<UStructProperty>(ParentProperty);
for (int32 ChildIndex = 0; ChildIndex < InNode.GetNumChildNodes(); ++ChildIndex)
{
TSharedPtr<FPropertyNode> ChildNodePtr = InNode.GetChildNode(ChildIndex);
FPropertyNode& ChildNode = *ChildNodePtr;
UProperty* Property = ChildNode.GetProperty();
{
FObjectPropertyNode* ObjNode = ChildNode.AsObjectNode();
FCategoryPropertyNode* CategoryNode = ChildNode.AsCategoryNode();
if (ObjNode)
{
// Currently object property nodes do not provide any useful information other than being a container for its children. We do not draw anything for them.
// When we encounter object property nodes, add their children instead of adding them to the tree.
UpdatePropertyMapRecursive(ChildNode, InDetailLayout, CurCategory, ObjNode);
}
else if (CategoryNode)
{
// For category nodes, we just set the current category and recurse through the children
UpdatePropertyMapRecursive(ChildNode, InDetailLayout, CategoryNode->GetCategoryName(), CurObjectNode);
}
else
{
// Whether or not the property can be visible in the default detail layout
bool bVisibleByDefault = IsVisibleStandaloneProperty(ChildNode, InNode);
// Whether or not the property is a struct
UStructProperty* StructProperty = Cast<UStructProperty>(Property);
bool bIsStruct = StructProperty != NULL;
static FName ShowOnlyInners("ShowOnlyInnerProperties");
bool bIsChildOfCustomizedStruct = false;
bool bIsCustomizedStruct = false;
const UStruct* Struct = StructProperty ? StructProperty->Struct : NULL;
const UStruct* ParentStruct = ParentStructProp ? ParentStructProp->Struct : NULL;
if (Struct || ParentStruct)
{
FPropertyEditorModule& ParentPlugin = FModuleManager::GetModuleChecked<FPropertyEditorModule>("PropertyEditor");
if (Struct)
{
bIsCustomizedStruct = ParentPlugin.IsCustomizedStruct(Struct, SharedThis( this ) );
}
if (ParentStruct)
{
bIsChildOfCustomizedStruct = ParentPlugin.IsCustomizedStruct(ParentStruct, SharedThis( this ) );
}
}
// Whether or not to push out struct properties to their own categories or show them inside an expandable struct
bool bPushOutStructProps = bIsStruct && !bIsCustomizedStruct && !ParentStructProp && Property->HasMetaData(ShowOnlyInners);
// Is the property edit inline new
const bool bIsEditInlineNew = SPropertyEditorEditInline::Supports(&ChildNode, ChildNode.GetArrayIndex());
// Is this a property of an array
bool bIsChildOfArray = PropertyEditorHelpers::IsChildOfArray(ChildNode);
// Edit inline new properties should be visible by default
bVisibleByDefault |= bIsEditInlineNew;
// Children of arrays are not visible directly,
bVisibleByDefault &= !bIsChildOfArray;
FPropertyAndParent PropertyAndParent(*Property, ParentProperty);
const bool bIsUserVisible = IsPropertyVisible(PropertyAndParent);
// Inners of customized in structs should not be taken into consideration for customizing. They are not designed to be individually customized when their parent is already customized
if (!bIsChildOfCustomizedStruct)
{
// Add any object classes with properties so we can ask them for custom property layouts later
ClassesWithProperties.Add(Property->GetOwnerStruct());
}
// If there is no outer object then the class is the object root and there is only one instance
FName InstanceName = NAME_None;
if (CurObjectNode && CurObjectNode->GetParentNode())
{
InstanceName = CurObjectNode->GetParentNode()->GetProperty()->GetFName();
}
else if (ParentStructProp)
{
InstanceName = ParentStructProp->GetFName();
}
// Do not add children of customized in struct properties or arrays
if (!bIsChildOfCustomizedStruct && !bIsChildOfArray)
{
// Get the class property map
FClassInstanceToPropertyMap& ClassInstanceMap = ClassToPropertyMap.FindOrAdd(Property->GetOwnerStruct()->GetFName());
FPropertyNodeMap& PropertyNodeMap = ClassInstanceMap.FindOrAdd(InstanceName);
if (!PropertyNodeMap.ParentProperty)
{
PropertyNodeMap.ParentProperty = CurObjectNode;
}
else
{
ensure(PropertyNodeMap.ParentProperty == CurObjectNode);
}
checkSlow(!PropertyNodeMap.Contains(Property->GetFName()));
PropertyNodeMap.Add(Property->GetFName(), ChildNodePtr);
}
if (bVisibleByDefault && bIsUserVisible && !bPushOutStructProps)
{
FName CategoryName = CurCategory;
// For properties inside a struct, add them to their own category unless they just take the name of the parent struct.
// In that case push them to the parent category
FName PropertyCatagoryName = FObjectEditorUtils::GetCategoryFName(Property);
if (!ParentStructProp || (PropertyCatagoryName != ParentStructProp->Struct->GetFName()))
{
CategoryName = PropertyCatagoryName;
}
if (IsPropertyReadOnly(PropertyAndParent))
{
ChildNode.SetNodeFlags(EPropertyNodeFlags::IsReadOnly, true);
}
// Add a property to the default category
FDetailCategoryImpl& CategoryImpl = InDetailLayout.DefaultCategory(CategoryName);
CategoryImpl.AddPropertyNode(ChildNodePtr.ToSharedRef(), InstanceName);
}
bool bRecurseIntoChildren =
!bIsChildOfCustomizedStruct // Don't recurse into built in struct children, we already know what they are and how to display them
&& !bIsCustomizedStruct // Don't recurse into customized structs
&& !bIsChildOfArray // Do not recurse into arrays, the children are drawn by the array property parent
&& !bIsEditInlineNew // Edit inline new children are not supported for customization yet
&& bIsUserVisible // Properties must be allowed to be visible by a user if they are not then their children are not visible either
&& (!bIsStruct || bPushOutStructProps); // Only recurse into struct properties if they are going to be displayed as standalone properties in categories instead of inside an expandable area inside a category
if (bRecurseIntoChildren)
{
// Built in struct properties or children of arras
UpdatePropertyMapRecursive(ChildNode, InDetailLayout, CurCategory, CurObjectNode);
}
}
}
}
}
void SDetailsViewBase::UpdatePropertyMap()
{
// Reset everything
ClassToPropertyMap.Empty();
ClassesWithProperties.Empty();
// We need to be able to create a new detail layout and properly clean up the old one in the process
check(!DetailLayout.IsValid() || DetailLayout.IsUnique());
RootTreeNodes.Empty();
DetailLayout = MakeShareable(new FDetailLayoutBuilderImpl(ClassToPropertyMap, PropertyUtilities.ToSharedRef(), SharedThis(this)));
auto RootPropertyNode = GetRootNode();
check(RootPropertyNode.IsValid());
// Currently object property nodes do not provide any useful information other than being a container for its children. We do not draw anything for them.
// When we encounter object property nodes, add their children instead of adding them to the tree.
UpdatePropertyMapRecursive(*RootPropertyNode, *DetailLayout, NAME_None, RootPropertyNode.Get());
CustomUpdatePropertyMap();
// Ask for custom detail layouts, unless disabled. One reason for disabling custom layouts is that the custom layouts
// inhibit our ability to find a single property's tree node. This is problematic for the diff and merge tools, that need
// to display and highlight each changed property for the user. We could whitelist 'good' customizations here if
// we can make them work with the diff/merge tools.
if( !bDisableCustomDetailLayouts )
{
QueryCustomDetailLayout(*DetailLayout);
}
DetailLayout->GenerateDetailLayout();
UpdateFilteredDetails();
}