// Copyright Epic Games, Inc. All Rights Reserved. #include "ItemPropertyNode.h" #include "Editor.h" #include "EditorMetadataOverrides.h" #include "ObjectPropertyNode.h" #include "PropertyEditorHelpers.h" #include "PropertyPathHelpers.h" #include "Settings/EditorStyleSettings.h" #include "UserInterface/PropertyEditor/SPropertyEditorArrayItem.h" #include "UObject/PropertyOptional.h" #include "VerseVM/VVMCVars.h" #define LOCTEXT_NAMESPACE "ItemPropertyNode" FItemPropertyNode::FItemPropertyNode() { bCanDisplayFavorite = false; } FItemPropertyNode::~FItemPropertyNode() { } uint8* FItemPropertyNode::GetValueBaseAddress(uint8* StartAddress, bool bIsSparseData, bool bIsStruct) const { const FProperty* MyProperty = GetProperty(); const TSharedPtr ParentNode = ParentNodeWeakPtr.Pin(); if (MyProperty && ParentNode) { const FArrayProperty* OuterArrayProp = MyProperty->GetOwner(); const FSetProperty* OuterSetProp = MyProperty->GetOwner(); const FMapProperty* OuterMapProp = MyProperty->GetOwner(); uint8* ValueBaseAddress = ParentNode->GetValueBaseAddress(StartAddress, bIsSparseData, bIsStruct); if (OuterArrayProp != nullptr) { FScriptArrayHelper ArrayHelper(OuterArrayProp, ValueBaseAddress); if (ValueBaseAddress != nullptr && ArrayHelper.IsValidIndex(ArrayIndex)) { return ArrayHelper.GetRawPtr(ArrayIndex); } } else if (OuterSetProp != nullptr) { FScriptSetHelper SetHelper(OuterSetProp, ValueBaseAddress); if (ValueBaseAddress != nullptr) { int32 ActualIndex = SetHelper.FindInternalIndex(ArrayIndex); if (ActualIndex != INDEX_NONE) { return SetHelper.GetElementPtr(ActualIndex); } } } else if (OuterMapProp != nullptr) { FScriptMapHelper MapHelper(OuterMapProp, ValueBaseAddress); if (ValueBaseAddress != nullptr) { int32 ActualIndex = MapHelper.FindInternalIndex(ArrayIndex); if (ActualIndex != INDEX_NONE) { uint8* PairPtr = MapHelper.GetPairPtr(ActualIndex); return MyProperty->ContainerPtrToValuePtr(PairPtr); } } } else { uint8* ValueAddress = ValueBaseAddress; if (ValueAddress != nullptr && ParentNode->GetProperty() != MyProperty) { // if this is not a fixed size array (in which the parent property and this property are the same), we need to offset from the property (otherwise, the parent already did that for us) ValueAddress = MyProperty->ContainerPtrToValuePtr(ValueAddress); } if (ValueAddress != nullptr) { ValueAddress += ArrayOffset; } return ValueAddress; } } return nullptr; } uint8* FItemPropertyNode::GetValueAddress(uint8* StartAddress, bool bIsSparseData, bool bIsStruct) const { uint8* Result = GetValueBaseAddress(StartAddress, bIsSparseData, bIsStruct); return Result; } /** * Overridden function for special setup */ void FItemPropertyNode::InitBeforeNodeFlags() { FPropertyNode::InitBeforeNodeFlags(); const FProperty* MyProperty = Property.Get(); const FObjectPropertyBase* ObjectProperty = CastField(MyProperty); if (ObjectProperty) { if (Verse::CVarUseDynamicSubobjectInstancing->GetBool()) { if (ObjectProperty->GetOwnerClass()->ShouldUseDynamicSubobjectInstancing()) { SetNodeFlags(EPropertyNodeFlags::SupportsDynamicInstancing, true); } } else { static const FName Name_SupportsDynamicInstance("SupportsDynamicInstance"); if (ObjectProperty->HasMetaData(Name_SupportsDynamicInstance)) { SetNodeFlags(EPropertyNodeFlags::SupportsDynamicInstancing, true); } } } } void FItemPropertyNode::InitExpansionFlags(void) { FProperty* MyProperty = GetProperty(); bool bExpandableType = CastField(MyProperty) || CastField(MyProperty) || CastField(MyProperty) || CastField(MyProperty) || CastField(MyProperty); auto IsDynamicInstanced = [this, MyProperty]() { if (!HasNodeFlags(EPropertyNodeFlags::SupportsDynamicInstancing)) { return false; } // Handle dynamic instancing // The property will instance the object under it only if that object is within this object's outer // otherwise the object will be a reference // The Property tree needs to dead end the generation of PropertyNodes for references to objects otherwise initializing // the child nodes amounts to effectively enumerating all paths to a given property on the referenced object which can have considerable // performance and memory issues FObjectProperty* ObjectProperty = CastField(MyProperty); if (!ObjectProperty) { return false; } // Walk up the PropertyNode tree to find the nearest ObjectNode, this will be the outer we check against const FObjectPropertyNode* ParentObjectNode = FindObjectItemParent(); if (!ParentObjectNode) { return false; } FReadAddressList ObjectAddresses; if( !GetReadAddress(!!HasNodeFlags(EPropertyNodeFlags::SingleSelectOnly), ObjectAddresses, false ) ) { return false; } if (ParentObjectNode->GetNumObjects() != ObjectAddresses.Num()) { // Cannot establish a one-to-one mapping of objects, so it is not possible to determine if the object is dynamic instanced return false; } // We've got some addresses, and we know they're all NULL or non-NULL. // Have a peek at them and check if they are instanced. They must all be instanced otherwise we dead-end the node expansion for (int32 i = 0; i < ObjectAddresses.Num(); i++) { const UObject* Outer = ParentObjectNode->GetUObject(i); UObject* Obj = ObjectProperty->GetObjectPropertyValue(ObjectAddresses.GetAddress(i)); if(Outer != nullptr && Obj != nullptr) { if (!Obj->IsInOuter(Outer)) { return false; } } else { return false; } } return true; }; const bool bIsDynamicInstanced = IsDynamicInstanced(); if (bIsDynamicInstanced) { SetNodeFlags(EPropertyNodeFlags::DynamicInstance, true); bExpandableType = true; } if (bExpandableType || HasNodeFlags(EPropertyNodeFlags::EditInlineNew) || HasNodeFlags(EPropertyNodeFlags::ShowInnerObjectProperties) || (MyProperty->ArrayDim > 1 && ArrayIndex == -1)) { SetNodeFlags(EPropertyNodeFlags::CanBeExpanded, true); } } /** * Overridden function for Creating Child Nodes */ void FItemPropertyNode::InitChildNodes() { //NOTE - this is only turned off as to not invalidate child object nodes. FProperty* MyProperty = GetProperty(); FStructProperty* StructProperty = CastField(MyProperty); FArrayProperty* ArrayProperty = CastField(MyProperty); FSetProperty* SetProperty = CastField(MyProperty); FMapProperty* MapProperty = CastField(MyProperty); FObjectPropertyBase* ObjectProperty = CastField(MyProperty); FOptionalProperty* OptionalProperty = CastField(MyProperty); const bool bShouldShowHiddenProperties = !!HasNodeFlags(EPropertyNodeFlags::ShouldShowHiddenProperties); const bool bShouldShowDisableEditOnInstance = !!HasNodeFlags(EPropertyNodeFlags::ShouldShowDisableEditOnInstance); if( MyProperty->ArrayDim > 1 && ArrayIndex == -1 ) { // Do not add array children which are defined by an enum but the enum at the array index is hidden // This only applies to static arrays static const FName NAME_ArraySizeEnum("ArraySizeEnum"); UEnum* ArraySizeEnum = NULL; if (MyProperty->HasMetaData(NAME_ArraySizeEnum)) { ArraySizeEnum = FindObject(NULL, *MyProperty->GetMetaData(NAME_ArraySizeEnum)); } // Expand array. for( int32 Index = 0 ; Index < MyProperty->ArrayDim ; Index++ ) { bool bShouldBeHidden = false; if( ArraySizeEnum ) { // The enum at this array index is hidden bShouldBeHidden = ArraySizeEnum->HasMetaData(TEXT("Hidden"), Index ); } if( !bShouldBeHidden ) { TSharedPtr NewItemNode( new FItemPropertyNode); FPropertyNodeInitParams InitParams; InitParams.ParentNode = SharedThis(this); InitParams.Property = MyProperty; InitParams.ArrayOffset = Index*MyProperty->GetElementSize(); InitParams.ArrayIndex = Index; InitParams.bAllowChildren = true; InitParams.bForceHiddenPropertyVisibility = bShouldShowHiddenProperties; InitParams.bCreateDisableEditOnInstanceNodes = bShouldShowDisableEditOnInstance; NewItemNode->InitNode( InitParams ); AddChildNode(NewItemNode); } } } else if( ArrayProperty ) { void* Array = NULL; FReadAddressList Addresses; if ( GetReadAddress(HasNodeFlags(EPropertyNodeFlags::SingleSelectOnly), Addresses ) ) { Array = Addresses.GetAddress(0); } if( Array ) { FScriptArrayHelper ArrayHelper(ArrayProperty, Array); for( int32 Index = 0 ; Index < ArrayHelper.Num(); Index++ ) { TSharedPtr NewItemNode( new FItemPropertyNode ); FPropertyNodeInitParams InitParams; InitParams.ParentNode = SharedThis(this); InitParams.Property = ArrayProperty->Inner; InitParams.ArrayOffset = Index * ArrayProperty->Inner->GetElementSize(); InitParams.ArrayIndex = Index; InitParams.bAllowChildren = true; InitParams.bForceHiddenPropertyVisibility = bShouldShowHiddenProperties; InitParams.bCreateDisableEditOnInstanceNodes = bShouldShowDisableEditOnInstance; NewItemNode->InitNode( InitParams ); AddChildNode(NewItemNode); } } } else if ( SetProperty ) { void* Set = NULL; FReadAddressList Addresses; if (GetReadAddress(!!HasNodeFlags(EPropertyNodeFlags::SingleSelectOnly), Addresses)) { Set = Addresses.GetAddress(0); } if (Set) { FScriptSetHelper SetHelper(SetProperty, Set); for (int32 Index = 0; Index < SetHelper.Num(); ++Index) { TSharedPtr NewItemNode(new FItemPropertyNode); FPropertyNodeInitParams InitParams; InitParams.ParentNode = SharedThis(this); InitParams.Property = SetProperty->ElementProp; InitParams.ArrayIndex = Index; InitParams.bAllowChildren = true; InitParams.bForceHiddenPropertyVisibility = bShouldShowHiddenProperties; InitParams.bCreateDisableEditOnInstanceNodes = bShouldShowDisableEditOnInstance; NewItemNode->InitNode(InitParams); AddChildNode(NewItemNode); } } } else if ( MapProperty ) { void* Map = NULL; FReadAddressList Addresses; if (GetReadAddress(!!HasNodeFlags(EPropertyNodeFlags::SingleSelectOnly), Addresses)) { Map = Addresses.GetAddress(0); } if ( Map ) { FScriptMapHelper MapHelper(MapProperty, Map); for (int32 Index = 0; Index < MapHelper.Num(); ++Index) { // Construct the key node first TSharedPtr KeyNode(new FItemPropertyNode()); FPropertyNodeInitParams InitParams; InitParams.ParentNode = SharedThis(this); // Key Node needs to point to this node to ensure its data is set correctly InitParams.Property = MapHelper.KeyProp; InitParams.ArrayIndex = Index; InitParams.ArrayOffset = 0; InitParams.bAllowChildren = true; InitParams.bForceHiddenPropertyVisibility = bShouldShowHiddenProperties; InitParams.bCreateDisableEditOnInstanceNodes = bShouldShowDisableEditOnInstance; KeyNode->InitNode(InitParams); // Not adding the key node as a child, otherwise it'll show up in the wrong spot. // Then the value node TSharedPtr ValueNode(new FItemPropertyNode()); // Reuse the init params InitParams.ParentNode = SharedThis(this); InitParams.Property = MapHelper.ValueProp; ValueNode->InitNode(InitParams); AddChildNode(ValueNode); FPropertyNode::SetupKeyValueNodePair(KeyNode, ValueNode); } } } else if( StructProperty ) { // Expand struct. TArray StructMembers; for (TFieldIterator It(StructProperty->Struct); It; ++It) { FProperty* StructMember = *It; if (PropertyEditorHelpers::ShouldBeVisible(*this, StructMember)) { StructMembers.Add(StructMember); } } PropertyEditorHelpers::OrderPropertiesFromMetadata(StructMembers); for (FProperty* StructMember : StructMembers) { TSharedPtr NewItemNode( new FItemPropertyNode ); FPropertyNodeInitParams InitParams; InitParams.ParentNode = SharedThis(this); InitParams.Property = StructMember; InitParams.ArrayOffset = 0; InitParams.ArrayIndex = INDEX_NONE; InitParams.bAllowChildren = true; InitParams.bForceHiddenPropertyVisibility = bShouldShowHiddenProperties; InitParams.bCreateDisableEditOnInstanceNodes = bShouldShowDisableEditOnInstance; NewItemNode->InitNode( InitParams ); AddChildNode(NewItemNode); if ( FPropertySettings::Get().ExpandDistributions() == false) { // auto-expand distribution structs if ( CastField(StructMember) || CastField(StructMember) || CastField(StructMember) || CastField(StructMember) ) { const FName StructName = StructProperty->Struct->GetFName(); if (StructName == NAME_RawDistributionFloat || StructName == NAME_RawDistributionVector) { NewItemNode->SetNodeFlags(EPropertyNodeFlags::Expanded, true); } } } } } else if( ObjectProperty ) { uint8* ReadValue = NULL; FReadAddressList ReadAddresses; if( GetReadAddress(!!HasNodeFlags(EPropertyNodeFlags::SingleSelectOnly), ReadAddresses, false ) ) { // We've got some addresses, and we know they're all NULL or non-NULL. // Have a peek at the first one, and only build an objects node if we've got addresses. if( UObject* Obj = (ReadAddresses.Num() > 0) ? ObjectProperty->GetObjectPropertyValue(ReadAddresses.GetAddress(0)) : nullptr ) { //verify it's not above in the hierarchy somewhere FObjectPropertyNode* ParentObjectNode = FindObjectItemParent(); while (ParentObjectNode) { for ( TPropObjectIterator Itor( ParentObjectNode->ObjectIterator() ) ; Itor ; ++Itor ) { if (*Itor == Obj) { SetNodeFlags(EPropertyNodeFlags::NoChildrenDueToCircularReference, true); //stop the circular loop!!! return; } } FPropertyNode* UpwardTravesalNode = ParentObjectNode->GetParentNode(); ParentObjectNode = (UpwardTravesalNode==NULL) ? NULL : UpwardTravesalNode->FindObjectItemParent(); } TSharedPtr NewObjectNode( new FObjectPropertyNode ); for ( int32 AddressIndex = 0 ; AddressIndex < ReadAddresses.Num() ; ++AddressIndex ) { NewObjectNode->AddObject( ObjectProperty->GetObjectPropertyValue(ReadAddresses.GetAddress(AddressIndex) ) ); } FPropertyNodeInitParams InitParams; InitParams.ParentNode = SharedThis(this); InitParams.Property = MyProperty; InitParams.ArrayOffset = 0; InitParams.ArrayIndex = INDEX_NONE; InitParams.bAllowChildren = true; InitParams.bForceHiddenPropertyVisibility = bShouldShowHiddenProperties; InitParams.bCreateDisableEditOnInstanceNodes = bShouldShowDisableEditOnInstance; NewObjectNode->InitNode( InitParams ); AddChildNode(NewObjectNode); } } } else if (OptionalProperty) { void* Optional = NULL; FReadAddressList Addresses; if (GetReadAddress(Addresses)) { for (int i = 0; i < Addresses.Num(); i++) { Optional = Addresses.GetAddress(i); if (OptionalProperty->IsSet(Optional)) { TSharedPtr ValueNode = MakeShared(); FPropertyNodeInitParams InitParams; InitParams.ParentNode = SharedThis(this); InitParams.Property = OptionalProperty->GetValueProperty(); InitParams.bAllowChildren = true; InitParams.bForceHiddenPropertyVisibility = !!HasNodeFlags(EPropertyNodeFlags::ShouldShowHiddenProperties); InitParams.bCreateDisableEditOnInstanceNodes = !!HasNodeFlags(EPropertyNodeFlags::ShouldShowDisableEditOnInstance); ValueNode->InitNode(InitParams); AddChildNode(ValueNode); } } } } } void FItemPropertyNode::SetFavorite(bool IsFavorite) { if (GEditor == nullptr) { return; } UEditorMetadataOverrides* MetadataOverrides = GEditor->GetEditorSubsystem(); if (MetadataOverrides == nullptr) { return; } const FObjectPropertyNode* ObjectParent = FindObjectItemParent(); if (ObjectParent == nullptr) { return; } FString Path; GetQualifiedName(Path, /*bWithArrayIndex=*/true, ObjectParent, /*bIgnoreCategories=*/true); static const FName FavoritePropertiesName("FavoriteProperties"); TArray FavoritePropertiesList; if (MetadataOverrides->GetArrayMetadata(ObjectParent->GetObjectBaseClass(), FavoritePropertiesName, FavoritePropertiesList)) { if (IsFavorite) { FavoritePropertiesList.AddUnique(Path); } else { FavoritePropertiesList.Remove(Path); } MetadataOverrides->SetArrayMetadata(ObjectParent->GetObjectBaseClass(), FavoritePropertiesName, FavoritePropertiesList); } else { if (IsFavorite) { FavoritePropertiesList.Add(Path); MetadataOverrides->SetArrayMetadata(ObjectParent->GetObjectBaseClass(), FavoritePropertiesName, FavoritePropertiesList); } } } bool FItemPropertyNode::IsFavorite() const { if (GEditor == nullptr) { return false; } UEditorMetadataOverrides* MetadataOverrides = GEditor->GetEditorSubsystem(); if (MetadataOverrides == nullptr) { return false; } const FObjectPropertyNode* ObjectParent = FindObjectItemParent(); if (ObjectParent == nullptr) { return false; } FString Path; GetQualifiedName(Path, /*bWithArrayIndex=*/true, ObjectParent, /*bIgnoreCategories=*/true); static const FName FavoritePropertiesName("FavoriteProperties"); TArray FavoritePropertiesList; if (MetadataOverrides->GetArrayMetadata(ObjectParent->GetObjectBaseClass(), FavoritePropertiesName, FavoritePropertiesList)) { return FavoritePropertiesList.Contains(Path); } return false; } void FItemPropertyNode::SetDisplayNameOverride( const FText& InDisplayNameOverride ) { DisplayNameOverride = InDisplayNameOverride; } FText FItemPropertyNode::GetDisplayName() const { FText FinalDisplayName; if( !DisplayNameOverride.IsEmpty() ) { FinalDisplayName = DisplayNameOverride; } else { const FProperty* PropertyPtr = GetProperty(); if( GetArrayIndex()==-1 && PropertyPtr != NULL ) { // This item is not a member of an array, get a traditional display name if ( FPropertySettings::Get().ShowFriendlyPropertyNames() ) { //We are in "readable display name mode"../ Make a nice name FinalDisplayName = PropertyPtr->GetDisplayNameText(); if ( FinalDisplayName.IsEmpty() ) { FString PropertyDisplayName; bool bIsBoolProperty = CastField(PropertyPtr) != NULL; const TSharedPtr ParentNode = ParentNodeWeakPtr.Pin(); const FStructProperty* ParentStructProperty = CastField(ParentNode->GetProperty()); if( ParentStructProperty && ParentStructProperty->Struct->GetFName() == NAME_Rotator ) { if( Property->GetFName() == "Roll" ) { PropertyDisplayName = TEXT("X"); } else if( Property->GetFName() == "Pitch" ) { PropertyDisplayName = TEXT("Y"); } else if( Property->GetFName() == "Yaw" ) { PropertyDisplayName = TEXT("Z"); } else { check(0); } } else { PropertyDisplayName = Property->GetName(); } if( GetDefault()->bShowFriendlyNames ) { PropertyDisplayName = FName::NameToDisplayString( PropertyDisplayName, bIsBoolProperty ); } FinalDisplayName = FText::FromString( PropertyDisplayName ); } } else { FinalDisplayName = FText::FromString( PropertyPtr->GetName() ); } } else if (const TSharedPtr ParentNode = ParentNodeWeakPtr.Pin()) { // Sets and maps do not have a display index. FProperty* ParentProperty = ParentNode->GetProperty(); // Get the ArraySizeEnum class from meta data. static const FName NAME_ArraySizeEnum("ArraySizeEnum"); UEnum* ArraySizeEnum = NULL; if (PropertyPtr && PropertyPtr->HasMetaData(NAME_ArraySizeEnum)) { ArraySizeEnum = FindObject(NULL, *Property->GetMetaData(NAME_ArraySizeEnum)); } // Also handle UArray's having the ArraySizeEnum entry... if (ArraySizeEnum == nullptr && CastField(ParentProperty) != nullptr && ParentProperty->HasMetaData(NAME_ArraySizeEnum)) { ArraySizeEnum = FindObject(NULL, *ParentProperty->GetMetaData(NAME_ArraySizeEnum)); } if (CastField(ParentProperty) == nullptr && CastField(ParentProperty) == nullptr) { if (PropertyPtr != nullptr) { // Check if this property has Title Property Meta static const FName NAME_TitleProperty = FName(TEXT("TitleProperty")); FString TitleProperty = PropertyPtr->GetMetaData(NAME_TitleProperty); if (!TitleProperty.IsEmpty()) { FItemPropertyNode* NonConstThis = const_cast(this); FReadAddressListData ReadAddress; GetReadAddressUncached(*NonConstThis, ReadAddress); const UStruct* PropertyStruct = nullptr; if (const FObjectProperty* ObjectProperty = CastField(PropertyPtr)) { // We do this so we get the classes *exact* value and not just the base from PropertyClass uint8* FoundAddress = ReadAddress.GetAddress(0); UObject* ObjectValue = FoundAddress != nullptr ? ObjectProperty->GetObjectPropertyValue(FoundAddress) : nullptr; if (ObjectValue != nullptr) { PropertyStruct = ObjectValue->GetClass(); } } // Find the property and get the right property handle if (PropertyStruct != nullptr) { const TSharedPtr ThisAsHandle = PropertyEditorHelpers::GetPropertyHandle(NonConstThis->AsShared(), nullptr, nullptr); TSharedPtr TitleFormatter = FTitleMetadataFormatter::TryParse(ThisAsHandle, TitleProperty); if (TitleFormatter) { TitleFormatter->GetDisplayText(FinalDisplayName); } } } } if(FinalDisplayName.IsEmpty()) { // This item is a member of an array, its display name is its index if (PropertyPtr == NULL || ArraySizeEnum == NULL) { if (ArraySizeEnum == nullptr) { FinalDisplayName = FText::AsNumber(GetArrayIndex()); } else { FinalDisplayName = ArraySizeEnum->GetDisplayNameTextByIndex(GetArrayIndex()); } } else { FinalDisplayName = ArraySizeEnum->GetDisplayNameTextByIndex(GetArrayIndex()); } } } // Maps should have display names that reflect the key and value types else if (PropertyPtr != nullptr && CastField(ParentProperty) != nullptr) { FText FormatText = GetPropertyKeyNode().IsValid() ? LOCTEXT("MapValueDisplayFormat", "Value ({0})") : LOCTEXT("MapKeyDisplayFormat", "Key ({0})"); FString TypeName; if (const FStructProperty* StructProp = CastField(PropertyPtr)) { // For struct props, use the name of the struct itself TypeName = StructProp->Struct->GetName(); } else if (const FEnumProperty* EnumProp = CastField(PropertyPtr)) { // For enum props, use the name of the enum if (EnumProp->GetEnum() != nullptr) { TypeName = EnumProp->GetEnum()->GetName(); } else { TypeName = TEXT("Enum"); } } else if(PropertyPtr->IsA()) { // For strings, actually return "String" and not "Str" TypeName = TEXT("String"); } else { // For any other property, get the type from the property class TypeName = PropertyPtr->GetClass()->GetName(); int32 EndIndex = TypeName.Find(TEXT("Property"), ESearchCase::IgnoreCase, ESearchDir::FromEnd); if (EndIndex != -1) { TypeName.MidInline(0, EndIndex, EAllowShrinking::No); } } if (FPropertySettings::Get().ShowFriendlyPropertyNames()) { TypeName = FName::NameToDisplayString(TypeName, false); } FinalDisplayName = FText::Format(FormatText, FText::FromString(TypeName)); } } } return FinalDisplayName; } void FItemPropertyNode::SetToolTipOverride( const FText& InToolTipOverride ) { ToolTipOverride = InToolTipOverride; } FText FItemPropertyNode::GetToolTipText() const { if(!ToolTipOverride.IsEmpty()) { return ToolTipOverride; } return PropertyEditorHelpers::GetToolTipText(GetProperty()); } #undef LOCTEXT_NAMESPACE