Files
UnrealEngineUWP/Engine/Source/Editor/PropertyEditor/Private/StructurePropertyNode.cpp
david bromberg 71e3da8dc8 Sequencer: Major integration of Universal Object Locators into Sequencer. This includes:
* Adding some first pass UI for editing the Universal Object Locators that are used to bind objects to tracks in Sequencer. This is currently in the form of:
   * An 'Add Empty Binding' item in the + Actor to Sequencer menu
   * A 'Binding Properties' sub-menu in the object binding track properties that allows you to modify the array of UOL's that bind objects to the track. This allows you to specifically select certain UOL types and then fill in the data that makes up that UOL. This is necessary for example for the FortAIPlayerPawn    binding we plan on adding for UEFN.
* Major refactor of MovieScene binding code to more seamlessly allow for the use of the Locator resolution, especially in the case of supporting locators that spawn actors, either in the case of editor preview actors or future runtime 'spawnables'. This was necessary to consolidate the cases where locator resolve params were being created to as few places as possible for simplicity.
* Add the concept of 'default' editor and runtime flags to locators to make it easier for Sequencer to know how to interact with bindings of different types. These flags are then stored with the binding in Sequencer and editable by users as necessary. Sequencer treats 'load'/'unload' in this case similar to 'spawn'/'despawn' and generally speaking resolving a locator with editor flags of 'load' will expect the locator to spawn an actor for preview, while resolving a locator with runtime flags of 'load' will expect the locator to create a 'spawnable' character.
* To manage lifetime of these preview/spawnable characters, created an interface for a 'LocatorSpawnedCache' which the locators interact with when creating or destroying an actor. This allows the resolver of locators to differentiate between cases where they expect multiple resolves of the same locator to create duplicate actors vs. resolve to the same actor and track the lifetimes of these spawned actors.
* Sequencer implements the above cache within its own object cache in evaluation state.
* Expand the role of the previously created Binding Lifetime track to trigger loading/unloading via locators in the cache. The Binding Lifetime track will now automatically be added to a binding if its locator type contains 'Load' as a default editor or runtime flag.
* Some other editor updates to support the above.

Future issues to be resolved:
* We want to more easily expose the editing of binding properties when necessary rather than having it buried in a sub-menu. Ideas for this include some kind of UI on the track itself, for example a combo box with binding types on it, as well as potentially the properties menu appearing on hovering over the possessable icon on the object binding track. We also want to expose properties based on selection, for example object binding properties, track properties, and section properties, in another external window.
* We want to eventually replace the 'Actor to Sequencer' and 'Assign Actor' menu with this once the UX is better.

The above was all tested with a Fortnite NPC Locator type which will be added in a separate changelist.

[REVIEW] [at]ue-sequencer
#jira UE-199299

[CL 30596833 by david bromberg in ue5-main branch]
2024-01-12 12:41:00 -05:00

306 lines
9.2 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "StructurePropertyNode.h"
#include "ItemPropertyNode.h"
#include "PropertyEditorHelpers.h"
void FStructurePropertyNode::InitChildNodes()
{
InternalInitChildNodes(FName());
}
void FStructurePropertyNode::InternalInitChildNodes(FName SinglePropertyName)
{
const bool bShouldShowHiddenProperties = !!HasNodeFlags(EPropertyNodeFlags::ShouldShowHiddenProperties);
const bool bShouldShowDisableEditOnInstance = !!HasNodeFlags(EPropertyNodeFlags::ShouldShowDisableEditOnInstance);
const UStruct* Struct = GetBaseStructure();
TArray<FProperty*> StructMembers;
for (TFieldIterator<FProperty> It(Struct); It; ++It)
{
FProperty* StructMember = *It;
if (PropertyEditorHelpers::ShouldBeVisible(*this, StructMember))
{
if (SinglePropertyName == NAME_None || StructMember->GetFName() == SinglePropertyName)
{
StructMembers.Add(StructMember);
if (SinglePropertyName != NAME_None)
{
break;
}
}
}
}
// Cache the init time base struct so that we can determine if the struct has changed.
// Store the cached base struct before calling AddChildNode() as they may call back to this node.
WeakCachedBaseStruct = Struct;
PropertyEditorHelpers::OrderPropertiesFromMetadata(StructMembers);
for (FProperty* StructMember : StructMembers)
{
TSharedPtr<FItemPropertyNode> NewItemNode(new FItemPropertyNode);
FPropertyNodeInitParams InitParams;
InitParams.ParentNode = SharedThis(this);
InitParams.Property = StructMember;
InitParams.ArrayOffset = 0;
InitParams.ArrayIndex = INDEX_NONE;
InitParams.bAllowChildren = SinglePropertyName == NAME_None;
InitParams.bForceHiddenPropertyVisibility = bShouldShowHiddenProperties;
InitParams.bCreateDisableEditOnInstanceNodes = bShouldShowDisableEditOnInstance;
InitParams.bCreateCategoryNodes = false;
NewItemNode->InitNode(InitParams);
AddChildNode(NewItemNode);
}
}
void FStructurePropertyNode::InitBeforeNodeFlags()
{
// Cache the base struct. It is used to check if the struct changes later.
// The struct will be cached on each call to InternalInitChildNodes() as well.
// We'll cache it here too, so that a FStructurePropertyNode which is initialized
// with "InitParams.bAllowChildren = false" has it properly set up.
WeakCachedBaseStruct = GetBaseStructure();
}
bool FStructurePropertyNode::GetReadAddressUncached(const FPropertyNode& InPropertyNode, FReadAddressListData& OutAddresses) const
{
if (!HasValidStructData())
{
return false;
}
check(StructProvider.IsValid());
const FProperty* InItemProperty = InPropertyNode.GetProperty();
if (!InItemProperty)
{
return false;
}
UStruct* OwnerStruct = InItemProperty->GetOwnerStruct();
if (!OwnerStruct || OwnerStruct->IsStructTrashed())
{
// Verify that the property is not part of an invalid trash class
return false;
}
TArray<TSharedPtr<FStructOnScope>> Instances;
StructProvider->GetInstances(Instances, WeakCachedBaseStruct.Get());
bool bHasData = false;
for (TSharedPtr<FStructOnScope>& Instance : Instances)
{
uint8* ReadAddress = Instance.IsValid() ? Instance->GetStructMemory() : nullptr;
if (ReadAddress)
{
OutAddresses.Add(nullptr, InPropertyNode.GetValueBaseAddress(ReadAddress, InPropertyNode.HasNodeFlags(EPropertyNodeFlags::IsSparseClassData) != 0, /*bIsStruct=*/true), /*bIsStruct=*/true);
bHasData = true;
}
}
return bHasData;
}
bool FStructurePropertyNode::GetReadAddressUncached(const FPropertyNode& InPropertyNode,
bool InRequiresSingleSelection,
FReadAddressListData* OutAddresses,
bool bComparePropertyContents,
bool bObjectForceCompare,
bool bArrayPropertiesCanDifferInSize) const
{
if (!HasValidStructData())
{
return false;
}
check(StructProvider.IsValid());
const FProperty* InItemProperty = InPropertyNode.GetProperty();
if (!InItemProperty)
{
return false;
}
const UStruct* OwnerStruct = InItemProperty->GetOwnerStruct();
if (!OwnerStruct || OwnerStruct->IsStructTrashed())
{
// Verify that the property is not part of an invalid trash class
return false;
}
bool bAllTheSame = true;
TArray<TSharedPtr<FStructOnScope>> Instances;
StructProvider->GetInstances(Instances, WeakCachedBaseStruct.Get());
if (Instances.IsEmpty())
{
return false;
}
if (bComparePropertyContents || bObjectForceCompare)
{
const bool bIsSparse = InPropertyNode.HasNodeFlags(EPropertyNodeFlags::IsSparseClassData) != 0;
const uint8* BaseAddress = nullptr;
const UStruct* BaseStruct = nullptr;
for (TSharedPtr<FStructOnScope>& Instance : Instances)
{
if (Instance.IsValid())
{
if (const UStruct* Struct = Instance->GetStruct())
{
if (const uint8* ReadAddress = InPropertyNode.GetValueBaseAddress(Instance->GetStructMemory(), bIsSparse, /*bIsStruct=*/true))
{
if (!BaseAddress)
{
BaseAddress = ReadAddress;
BaseStruct = Struct;
}
else
{
if (BaseStruct != Struct)
{
bAllTheSame = false;
break;
}
if (!InItemProperty->Identical(BaseAddress, ReadAddress))
{
bAllTheSame = false;
break;
}
}
}
}
}
}
// If none of the instances have data, treat it as if the instance data was empty.
if (!BaseStruct)
{
bAllTheSame = false;
}
}
else
{
// Check that all are valid or invalid.
const UStruct* BaseStruct = Instances[0].IsValid() ? Instances[0]->GetStruct() : nullptr;
for (int32 Index = 1; Index < Instances.Num(); Index++)
{
const UStruct* Struct = Instances[Index].IsValid() ? Instances[Index]->GetStruct() : nullptr;
if (BaseStruct != Struct)
{
bAllTheSame = false;
break;
}
}
// If none of the instances have data, treat it as if the instance data was empty.
if (!BaseStruct)
{
bAllTheSame = false;
}
}
if (bAllTheSame && OutAddresses)
{
for (TSharedPtr<FStructOnScope>& Instance : Instances)
{
uint8* ReadAddress = Instance.IsValid() ? Instance->GetStructMemory() : nullptr;
if (ReadAddress)
{
OutAddresses->Add(nullptr, InPropertyNode.GetValueBaseAddress(ReadAddress, InPropertyNode.HasNodeFlags(EPropertyNodeFlags::IsSparseClassData) != 0, /*bIsStruct=*/true), /*bIsStruct=*/true);
}
}
}
return bAllTheSame;
}
uint8* FStructurePropertyNode::GetValueBaseAddress(uint8* StartAddress, bool bIsSparseData, bool bIsStruct) const
{
// If called with struct data, we expect that it is compatible with the first structure node down in the property node chain, return the address as is.
// This gets called usually when the calling code is dealing with a parent complex node.
if (bIsStruct)
{
return StartAddress;
}
if (StructProvider)
{
// Assume that this code gets called with an object or object sparse data.
//
// The passed object might not be the one that contains the values provided by struct provider.
// For example this function might get called on an edited object's template object.
// In that case the data structure is expected to match between the data edited by this node and the foreign object.
// If the structure provider is set up as indirection, then it knows how to translate parent node's value address to
// new value address even on data that is not the same as in the structure provider.
if (StructProvider->IsPropertyIndirection())
{
const TSharedPtr<FPropertyNode> ParentNode = ParentNodeWeakPtr.Pin();
if (!ensureMsgf(ParentNode, TEXT("Expecting valid parent node when indirection structure provider is called with Object data.")))
{
return nullptr;
}
// Resolve from parent nodes data.
uint8* ParentValueAddress = ParentNode->GetValueAddress(StartAddress, bIsSparseData);
uint8* ValueAddress = StructProvider->GetValueBaseAddress(ParentValueAddress, WeakCachedBaseStruct.Get());
return ValueAddress;
}
// The struct is really standalone, in which case we always return the standalone struct data.
// In that case we can only support one instance, since we cannot discern them.
// Note: Multiple standalone structure instances are supported when bIsStruct is true (e.g. when the structure property is root node).
TArray<TSharedPtr<FStructOnScope>> Instances;
StructProvider->GetInstances(Instances, WeakCachedBaseStruct.Get());
ensureMsgf(Instances.Num() <= 1, TEXT("Expecting max one instance on standalone structure provider."));
if (Instances.Num() == 1 && Instances[0].IsValid())
{
return Instances[0]->GetStructMemory();
}
}
return nullptr;
}
TSharedPtr<FPropertyNode> FStructurePropertyNode::GenerateSingleChild(FName ChildPropertyName)
{
constexpr bool bDestroySelf = false;
DestroyTree(bDestroySelf);
// No category nodes should be created in single property mode
SetNodeFlags(EPropertyNodeFlags::ShowCategories, false);
InternalInitChildNodes(ChildPropertyName);
if (ChildNodes.Num() > 0)
{
// only one node should be been created
check(ChildNodes.Num() == 1);
return ChildNodes[0];
}
return nullptr;
}
EPropertyDataValidationResult FStructurePropertyNode::EnsureDataIsValid()
{
CachedReadAddresses.Reset();
// If the struct has changed, rebuild children.
const UStruct* CachedBaseStruct = WeakCachedBaseStruct.Get();
if (GetBaseStructure() != CachedBaseStruct)
{
RebuildChildren();
return EPropertyDataValidationResult::ChildrenRebuilt;
}
return FPropertyNode::EnsureDataIsValid();
}