Files
UnrealEngine/Engine/Source/Editor/PropertyEditor/Private/SDetailSingleItemRow.cpp
2025-05-18 13:04:45 +08:00

1976 lines
64 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "SDetailSingleItemRow.h"
#include "Algo/AnyOf.h"
#include "Algo/Compare.h"
#include "DetailGroup.h"
#include "DetailPropertyRow.h"
#include "DetailsNameWidgetOverrideCustomization.h"
#include "DetailWidgetRow.h"
#include "Editor.h"
#include "IDetailDragDropHandler.h"
#include "IDetailPropertyExtensionHandler.h"
#include "Modules/ModuleInterface.h"
#include "Modules/ModuleManager.h"
#include "ObjectPropertyNode.h"
#include "PropertyEditorClipboard.h"
#include "PropertyEditorClipboardPrivate.h"
#include "PropertyEditorCopyPastePrivate.h"
#include "PropertyEditorModule.h"
#include "PropertyHandleImpl.h"
#include "PropertyPermissionList.h"
#include "SConstrainedBox.h"
#include "SDetailExpanderArrow.h"
#include "SDetailRowIndent.h"
#include "Styling/StyleColors.h"
#include "UObject/Field.h"
#include "UserInterface/PropertyEditor/PropertyEditorConstants.h"
#include "Widgets/Images/SImage.h"
#include "Widgets/Input/SButton.h"
#include "Widgets/Input/SComboButton.h"
#include "DetailsViewStyle.h"
#include "SDetailsView.h"
#include "ToolMenus.h"
namespace DetailWidgetConstants
{
const FMargin LeftRowPadding( 20.0f, 0.0f, 10.0f, 0.0f );
const FMargin RightRowPadding( 12.0f, 0.0f, 2.0f, 0.0f );
}
namespace SDetailSingleItemRow_Helper
{
// Get the node item number in case it is expand we have to recursively count all expanded children
void RecursivelyGetItemShow(TSharedRef<FDetailTreeNode> ParentItem, int32& ItemShowNum)
{
if (ParentItem->GetVisibility() == ENodeVisibility::Visible)
{
ItemShowNum++;
}
if (ParentItem->ShouldBeExpanded())
{
TArray< TSharedRef<FDetailTreeNode> > Childrens;
ParentItem->GetChildren(Childrens);
for (TSharedRef<FDetailTreeNode> ItemChild : Childrens)
{
RecursivelyGetItemShow(ItemChild, ItemShowNum);
}
}
}
}
void SDetailSingleItemRow::OnArrayOrCustomDragLeave(const FDragDropEvent& DragDropEvent)
{
TSharedPtr<FDecoratedDragDropOp> DecoratedOp = DragDropEvent.GetOperationAs<FDecoratedDragDropOp>();
if (DecoratedOp.IsValid())
{
DecoratedOp->ResetToDefaultToolTip();
}
}
/** Compute the new index to which to move the item when it's dropped onto the given index/drop zone. */
static int32 ComputeNewIndex(int32 OriginalIndex, int32 DropOntoIndex, EItemDropZone DropZone)
{
check(DropZone != EItemDropZone::OntoItem);
int32 NewIndex = DropOntoIndex;
if (DropZone == EItemDropZone::BelowItem)
{
// If the drop zone is below, then we actually move it to the next item's index
NewIndex++;
}
if (OriginalIndex < NewIndex)
{
// If the item is moved down the list, then all the other elements below it are shifted up one
NewIndex--;
}
return ensure(NewIndex >= 0) ? NewIndex : 0;
}
bool SDetailSingleItemRow::CheckValidDrop(const TSharedPtr<SDetailSingleItemRow> RowPtr, EItemDropZone DropZone) const
{
// Can't drop onto another array item; need to drop above or below
if (DropZone == EItemDropZone::OntoItem)
{
return false;
}
TSharedPtr<FPropertyNode> SwappingPropertyNode = RowPtr->SwappablePropertyNode;
if (SwappingPropertyNode.IsValid() && SwappablePropertyNode.IsValid() && SwappingPropertyNode != SwappablePropertyNode)
{
const int32 OriginalIndex = SwappingPropertyNode->GetArrayIndex();
const int32 NewIndex = ComputeNewIndex(OriginalIndex, SwappablePropertyNode->GetArrayIndex(), DropZone);
if (OriginalIndex != NewIndex)
{
TSharedPtr<IDetailsViewPrivate> DetailsView = OwnerTreeNode.Pin()->GetDetailsViewSharedPtr();
TSharedPtr<IPropertyHandle> SwappingHandle = PropertyEditorHelpers::GetPropertyHandle(SwappingPropertyNode.ToSharedRef(), DetailsView->GetNotifyHook(), DetailsView->GetPropertyUtilities());
TSharedPtr<IPropertyHandleArray> ParentHandle = SwappingHandle->GetParentHandle()->AsArray();
if (ParentHandle.IsValid() && SwappablePropertyNode->GetParentNode() == SwappingPropertyNode->GetParentNode())
{
return true;
}
}
}
return false;
}
FReply SDetailSingleItemRow::OnArrayAcceptDrop(const FDragDropEvent& DragDropEvent, EItemDropZone DropZone, TSharedPtr<FDetailTreeNode> TargetItem)
{
TSharedPtr<FArrayRowDragDropOp> ArrayDropOp = DragDropEvent.GetOperationAs< FArrayRowDragDropOp >();
if (!ArrayDropOp.IsValid())
{
return FReply::Unhandled();
}
TSharedPtr<SDetailSingleItemRow> RowPtr = ArrayDropOp->Row.Pin();
if (!RowPtr.IsValid())
{
return FReply::Unhandled();
}
if (!CheckValidDrop(RowPtr, DropZone))
{
return FReply::Unhandled();
}
TSharedPtr<IDetailsViewPrivate> DetailsView = OwnerTreeNode.Pin()->GetDetailsViewSharedPtr();
TSharedPtr<FPropertyNode> SwappingPropertyNode = RowPtr->SwappablePropertyNode;
TSharedPtr<IPropertyHandle> SwappingHandle = PropertyEditorHelpers::GetPropertyHandle(SwappingPropertyNode.ToSharedRef(), DetailsView->GetNotifyHook(), DetailsView->GetPropertyUtilities());
TSharedPtr<IPropertyHandleArray> ParentHandle = SwappingHandle->GetParentHandle()->AsArray();
const int32 OriginalIndex = SwappingPropertyNode->GetArrayIndex();
const int32 NewIndex = ComputeNewIndex(OriginalIndex, SwappablePropertyNode->GetArrayIndex(), DropZone);
// Need to swap the moving and target expansion states before saving
bool bOriginalSwappableExpansion = SwappablePropertyNode->HasNodeFlags(EPropertyNodeFlags::Expanded) != 0;
bool bOriginalSwappingExpansion = SwappingPropertyNode->HasNodeFlags(EPropertyNodeFlags::Expanded) != 0;
SwappablePropertyNode->SetNodeFlags(EPropertyNodeFlags::Expanded, bOriginalSwappingExpansion);
SwappingPropertyNode->SetNodeFlags(EPropertyNodeFlags::Expanded, bOriginalSwappableExpansion);
DetailsView->SaveExpandedItems(SwappablePropertyNode->GetParentNodeSharedPtr().ToSharedRef());
FScopedTransaction Transaction(NSLOCTEXT("UnrealEd", "MoveRow", "Move Row"));
SwappingHandle->GetParentHandle()->NotifyPreChange();
ParentHandle->MoveElementTo(OriginalIndex, NewIndex);
FPropertyChangedEvent MoveEvent(SwappingHandle->GetParentHandle()->GetProperty(), EPropertyChangeType::ArrayMove);
SwappingHandle->GetParentHandle()->NotifyPostChange(EPropertyChangeType::ArrayMove);
if (DetailsView->GetPropertyUtilities().IsValid())
{
DetailsView->GetPropertyUtilities()->NotifyFinishedChangingProperties(MoveEvent);
}
return FReply::Handled();
}
TOptional<EItemDropZone> SDetailSingleItemRow::OnArrayCanAcceptDrop(const FDragDropEvent& DragDropEvent, EItemDropZone DropZone, TSharedPtr<FDetailTreeNode> TargetItem)
{
TSharedPtr<FArrayRowDragDropOp> ArrayDropOp = DragDropEvent.GetOperationAs< FArrayRowDragDropOp >();
if (!ArrayDropOp.IsValid())
{
return TOptional<EItemDropZone>();
}
TSharedPtr<SDetailSingleItemRow> RowPtr = ArrayDropOp->Row.Pin();
if (!RowPtr.IsValid())
{
return TOptional<EItemDropZone>();
}
// Can't drop onto another array item, so recompute our own drop zone to ensure it's above or below
const FGeometry& Geometry = GetTickSpaceGeometry();
const float LocalPointerY = Geometry.AbsoluteToLocal(DragDropEvent.GetScreenSpacePosition()).Y;
const EItemDropZone OverrideDropZone = LocalPointerY < Geometry.GetLocalSize().Y * 0.5f ? EItemDropZone::AboveItem : EItemDropZone::BelowItem;
const bool IsValidDrop = CheckValidDrop(RowPtr, OverrideDropZone);
ArrayDropOp->SetValidTarget(IsValidDrop);
if (!IsValidDrop)
{
return TOptional<EItemDropZone>();
}
return OverrideDropZone;
}
FReply SDetailSingleItemRow::OnArrayHeaderAcceptDrop(const FDragDropEvent& DragDropEvent, EItemDropZone DropZone, TSharedPtr< FDetailTreeNode > Type)
{
OnArrayOrCustomDragLeave(DragDropEvent);
return FReply::Handled();
}
FReply SDetailSingleItemRow::OnCustomAcceptDrop(const FDragDropEvent& DragDropEvent, EItemDropZone DropZone, TSharedPtr<FDetailTreeNode> TargetItem)
{
// This should only be registered as a delegate if there's a custom handler
if (ensure(WidgetRow.CustomDragDropHandler))
{
if (WidgetRow.CustomDragDropHandler->AcceptDrop(DragDropEvent, DropZone))
{
return FReply::Handled();
}
}
return FReply::Unhandled();
}
TOptional<EItemDropZone> SDetailSingleItemRow::OnCustomCanAcceptDrop(const FDragDropEvent& DragDropEvent, EItemDropZone DropZone, TSharedPtr<FDetailTreeNode> Type)
{
// This should only be registered as a delegate if there's a custom handler
if (ensure(WidgetRow.CustomDragDropHandler))
{
return WidgetRow.CustomDragDropHandler->CanAcceptDrop(DragDropEvent, DropZone);
}
return TOptional<EItemDropZone>();
}
TSharedPtr<FPropertyNode> SDetailSingleItemRow::GetPropertyNode() const
{
TSharedPtr<FPropertyNode> PropertyNode = Customization->GetPropertyNode();
if (!PropertyNode.IsValid() && Customization->DetailGroup.IsValid())
{
PropertyNode = Customization->DetailGroup->GetHeaderPropertyNode();
}
// See if a custom builder has an associated node
if (!PropertyNode.IsValid() && Customization->HasCustomBuilder())
{
TSharedPtr<IPropertyHandle> PropertyHandle = Customization->CustomBuilderRow->GetPropertyHandle();
if (PropertyHandle.IsValid())
{
PropertyNode = StaticCastSharedPtr<FPropertyHandleBase>(PropertyHandle)->GetPropertyNode();
}
}
return PropertyNode;
}
TSharedPtr<IPropertyHandle> SDetailSingleItemRow::GetPropertyHandle() const
{
TSharedPtr<IPropertyHandle> Handle;
if (const TSharedPtr<FPropertyNode> PropertyNode = GetPropertyNode())
{
if (const TSharedPtr<FDetailTreeNode> OwnerTreeNodePtr = OwnerTreeNode.Pin())
{
if (TSharedPtr<IDetailsViewPrivate> DetailsView = OwnerTreeNodePtr->GetDetailsViewSharedPtr())
{
Handle = PropertyEditorHelpers::GetPropertyHandle(PropertyNode.ToSharedRef(), DetailsView->GetNotifyHook(), DetailsView->GetPropertyUtilities());
}
}
}
else if (WidgetRow.PropertyHandles.Num() > 0)
{
// @todo: Handle more than 1 property handle?
Handle = WidgetRow.PropertyHandles[0];
}
return Handle;
}
void SDetailSingleItemRow::UpdateResetToDefault()
{
bCachedResetToDefaultVisible = false;
TSharedPtr<IPropertyHandle> PropertyHandle = GetPropertyHandle();
if (WidgetRow.CustomResetToDefault.IsSet())
{
bCachedResetToDefaultVisible = WidgetRow.CustomResetToDefault.GetValue().IsResetToDefaultVisible(PropertyHandle);
return;
}
if (PropertyHandle.IsValid())
{
if (PropertyHandle->HasMetaData("NoResetToDefault") || PropertyHandle->GetInstanceMetaData("NoResetToDefault"))
{
bCachedResetToDefaultVisible = false;
return;
}
bCachedResetToDefaultVisible = PropertyHandle->CanResetToDefault();
}
}
void SDetailSingleItemRow::Construct( const FArguments& InArgs, FDetailLayoutCustomization* InCustomization, bool bHasMultipleColumns, TSharedRef<FDetailTreeNode> InOwnerTreeNode, const TSharedRef<STableViewBase>& InOwnerTableView )
{
OwnerTreeNode = InOwnerTreeNode;
bAllowFavoriteSystem = InArgs._AllowFavoriteSystem;
Customization = InCustomization;
TSharedRef<SWidget> Widget = SNullWidget::NullWidget;
FOnTableRowDragLeave DragLeaveDelegate;
FOnAcceptDrop AcceptDropDelegate;
FOnCanAcceptDrop CanAcceptDropDelegate;
TSharedPtr<IDetailsViewPrivate> DetailsView = InOwnerTreeNode->GetDetailsViewSharedPtr();
const FDetailColumnSizeData& ColumnSizeData = DetailsView ? DetailsView->GetColumnSizeData() : FDetailColumnSizeData();
PulseAnimation.AddCurve(0.0f, UE::PropertyEditor::Private::PulseAnimationLength, ECurveEaseFunction::CubicInOut);
// Play on construction if animation was started from a behavior the re-constructs this widget
const TSharedPtr<FPropertyNode> AssociatedPropertyNode = GetPropertyNode();
if (DetailsView && DetailsView->IsNodeAnimating(AssociatedPropertyNode))
{
// Resume from the stored animation time if it's still animating
float AnimationTime = DetailsView->GetNodeAnimationTime(AssociatedPropertyNode);
PulseAnimation.Play(SharedThis(this), false, AnimationTime);
}
TSharedPtr<FDetailCategoryImpl> Category = InOwnerTreeNode->GetParentCategory();
const bool bIsValidTreeNode = InOwnerTreeNode->GetParentCategory().IsValid() && InOwnerTreeNode->GetParentCategory()->IsParentLayoutValid();
if (bIsValidTreeNode)
{
if (Customization->IsValidCustomization())
{
TSharedPtr<FDetailGroup> Group = nullptr;
WidgetRow = Customization->GetWidgetRow();
// Populate the extension content in the WidgetRow if there's an extension handler.
PopulateExtensionWidget();
// Setup copy / paste actions
{
if (WidgetRow.IsCopyPasteBound())
{
CopyAction = WidgetRow.CopyMenuAction;
PasteAction = WidgetRow.PasteMenuAction;
}
else
{
TSharedPtr<FPropertyNode> PropertyNode = GetPropertyNode();
static const FName DisableCopyPasteMetaDataName("DisableCopyPaste");
if (PropertyNode.IsValid() && !PropertyNode->ParentOrSelfHasMetaData(DisableCopyPasteMetaDataName))
{
CopyAction.ExecuteAction = FExecuteAction::CreateSP(this, &SDetailSingleItemRow::OnCopyProperty);
PasteAction.ExecuteAction = FExecuteAction::CreateSP(this, &SDetailSingleItemRow::OnPasteProperty);
PasteAction.CanExecuteAction = FCanExecuteAction::CreateSP(this, &SDetailSingleItemRow::CanPasteProperty);
}
else if (Group = Customization->DetailGroup;
Group.IsValid())
{
CopyAction.ExecuteAction = FExecuteAction::CreateSP(this, &SDetailSingleItemRow::OnCopyGroup);
CopyAction.CanExecuteAction = FCanExecuteAction::CreateSP(this, &SDetailSingleItemRow::CanCopyGroup);
PasteAction.ExecuteAction = FExecuteAction::CreateSP(this, &SDetailSingleItemRow::OnPasteGroup);
PasteAction.CanExecuteAction = FCanExecuteAction::CreateSP(this, &SDetailSingleItemRow::CanPasteGroup);
}
else
{
CopyAction.ExecuteAction = FExecuteAction::CreateLambda([]() {});
CopyAction.CanExecuteAction = FCanExecuteAction::CreateLambda([]() { return false; });
PasteAction.ExecuteAction = FExecuteAction::CreateLambda([]() {});
PasteAction.CanExecuteAction = FCanExecuteAction::CreateLambda([]() { return false; });
}
}
if (WidgetRow.IsPasteFromTextBound())
{
OnPasteFromTextDelegate = WidgetRow.OnPasteFromTextDelegate.Pin();
}
else if (Category.IsValid())
{
OnPasteFromTextDelegate = Category->OnPasteFromText();
}
// If still not set, but this is a group, initialize
else if (Group.IsValid())
{
if (TSharedPtr<FOnPasteFromText> GroupOnPasteFromTextDelegate = Group->OnPasteFromText())
{
OnPasteFromTextDelegate = GroupOnPasteFromTextDelegate;
}
}
if (OnPasteFromTextDelegate.IsValid())
{
OnPasteFromTextDelegate->AddSP(this, &SDetailSingleItemRow::OnPasteFromText);
}
}
TSharedPtr<SWidget> NameWidget = WidgetRow.NameWidget.Widget;
TSharedPtr<SWidget> ValueWidget =
SNew(SConstrainedBox)
.MinWidth(WidgetRow.ValueWidget.MinWidth)
.MaxWidth(WidgetRow.ValueWidget.MaxWidth)
[
WidgetRow.ValueWidget.Widget
];
TSharedPtr<SWidget> ExtensionWidget = WidgetRow.ExtensionWidget.Widget;
// copies of attributes for lambda captures
TAttribute<bool> PropertyEnabledAttribute = InOwnerTreeNode->IsPropertyEditingEnabled();
TAttribute<bool> RowEditConditionAttribute = WidgetRow.EditConditionValue;
TAttribute<bool> RowIsEnabledAttribute = WidgetRow.IsEnabledAttr;
TAttribute<bool> IsEnabledAttribute = TAttribute<bool>::CreateLambda(
[PropertyEnabledAttribute, RowIsEnabledAttribute, RowEditConditionAttribute]()
{
return PropertyEnabledAttribute.Get(true) && RowIsEnabledAttribute.Get(true) && RowEditConditionAttribute.Get(true);
});
TAttribute<bool> RowIsValueEnabledAttribute = WidgetRow.IsValueEnabledAttr;
TAttribute<bool> IsValueEnabledAttribute = TAttribute<bool>::CreateLambda(
[IsEnabledAttribute, RowIsValueEnabledAttribute]()
{
return IsEnabledAttribute.Get() && RowIsValueEnabledAttribute.Get(true);
});
NameWidget->SetEnabled(IsEnabledAttribute);
ValueWidget->SetEnabled(IsValueEnabledAttribute);
ExtensionWidget->SetEnabled(IsEnabledAttribute);
TSharedRef<SSplitter> Splitter = SNew(SSplitter)
.Style(FAppStyle::Get(), "DetailsView.Splitter")
.PhysicalSplitterHandleSize(1.0f)
.HitDetectionSplitterHandleSize(5.0f)
.HighlightedHandleIndex(ColumnSizeData.GetHoveredSplitterIndex())
.OnHandleHovered(ColumnSizeData.GetOnSplitterHandleHovered());
Widget = SNew(SBorder)
.BorderImage(FAppStyle::Get().GetBrush("DetailsView.CategoryMiddle"))
.BorderBackgroundColor(this, &SDetailSingleItemRow::GetInnerBackgroundColor)
.Padding(0.0f)
[
Splitter
];
// create Name column:
// | Name | Value | Right |
TSharedRef<SHorizontalBox> NameColumnBox = SNew(SHorizontalBox)
.Clipping(EWidgetClipping::OnDemand);
// indentation and expander arrow
NameColumnBox->AddSlot()
.HAlign(HAlign_Left)
.VAlign(VAlign_Fill)
.Padding(0.0f)
.AutoWidth()
[
SNew(SDetailRowIndent, SharedThis(this))
];
if (WidgetRow.CustomDragDropHandler)
{
TSharedPtr<SDetailSingleItemRow> InRow = SharedThis(this);
// Let the handler subclasses decide if they want the handle widget.
if (WidgetRow.CustomDragDropHandler->UseHandleWidget())
{
TSharedRef<SWidget> ReorderHandle = PropertyEditorHelpers::MakePropertyReorderHandle(InRow, IsEnabledAttribute);
NameColumnBox->AddSlot()
.HAlign(HAlign_Left)
.VAlign(VAlign_Center)
.Padding(-4.0f, 0.0f, -10.0f, 0.0f)
.AutoWidth()
[
ReorderHandle
];
}
DragLeaveDelegate = FOnTableRowDragLeave::CreateSP(this, &SDetailSingleItemRow::OnArrayOrCustomDragLeave);
AcceptDropDelegate = FOnAcceptDrop::CreateSP(this, &SDetailSingleItemRow::OnCustomAcceptDrop);
CanAcceptDropDelegate = FOnCanAcceptDrop::CreateSP(this, &SDetailSingleItemRow::OnCustomCanAcceptDrop);
}
else if (TSharedPtr<FPropertyNode> PropertyNode = Customization->GetPropertyNode())
{
if (PropertyNode->IsReorderable())
{
TSharedPtr<SDetailSingleItemRow> InRow = SharedThis(this);
TSharedRef<SWidget> ArrayHandle = PropertyEditorHelpers::MakePropertyReorderHandle(InRow, IsEnabledAttribute);
NameColumnBox->AddSlot()
.HAlign(HAlign_Left)
.VAlign(VAlign_Center)
.Padding(-4.0f, 0.0f, -10.0f, 0.0f)
.AutoWidth()
[
ArrayHandle
];
SwappablePropertyNode = PropertyNode;
}
if (PropertyNode->IsReorderable() ||
(CastField<FArrayProperty>(PropertyNode->GetProperty()) != nullptr &&
CastField<FObjectProperty>(CastField<FArrayProperty>(PropertyNode->GetProperty())->Inner) != nullptr)) // Is an object array
{
DragLeaveDelegate = FOnTableRowDragLeave::CreateSP(this, &SDetailSingleItemRow::OnArrayOrCustomDragLeave);
AcceptDropDelegate = FOnAcceptDrop::CreateSP(this, PropertyNode->IsReorderable() ? &SDetailSingleItemRow::OnArrayAcceptDrop : &SDetailSingleItemRow::OnArrayHeaderAcceptDrop);
CanAcceptDropDelegate = FOnCanAcceptDrop::CreateSP(this, &SDetailSingleItemRow::OnArrayCanAcceptDrop);
}
}
NameColumnBox->AddSlot()
.HAlign(HAlign_Left)
.VAlign(VAlign_Center)
.Padding(2.0f,0.0f,0.0f,0.0f)
.AutoWidth()
[
SNew(SDetailExpanderArrow, SharedThis(this))
];
NameColumnBox->AddSlot()
.VAlign(VAlign_Center)
.HAlign(HAlign_Left)
.Padding(2.0f,0.0f,0.0f,0.0f)
.AutoWidth()
[
SNew(SEditConditionWidget)
.EditConditionValue(WidgetRow.EditConditionValue)
.OnEditConditionValueChanged(WidgetRow.OnEditConditionValueChanged)
];
if (bHasMultipleColumns)
{
NameColumnBox->AddSlot()
.HAlign(WidgetRow.NameWidget.HorizontalAlignment)
.VAlign(WidgetRow.NameWidget.VerticalAlignment)
.Padding(2.0f,0.0f,0.0f,0.0f)
[
NameWidget.ToSharedRef()
];
// create Name column:
// | Name | Value | Right |
Splitter->AddSlot()
.Value(ColumnSizeData.GetNameColumnWidth())
.OnSlotResized(ColumnSizeData.GetOnNameColumnResized())
[
GetNameWidget(NameColumnBox, GetPropertyNode())
];
// create Value column:
// | Name | Value | Right |
Splitter->AddSlot()
.Value(ColumnSizeData.GetValueColumnWidth())
.OnSlotResized(ColumnSizeData.GetOnValueColumnResized())
[
SNew(SHorizontalBox)
.Clipping(EWidgetClipping::OnDemand)
+ SHorizontalBox::Slot()
.HAlign(WidgetRow.ValueWidget.HorizontalAlignment)
.VAlign(WidgetRow.ValueWidget.VerticalAlignment)
.Padding(DetailWidgetConstants::RightRowPadding)
[
ValueWidget.ToSharedRef()
]
// extension widget
+ SHorizontalBox::Slot()
.HAlign(WidgetRow.ExtensionWidget.HorizontalAlignment)
.VAlign(WidgetRow.ExtensionWidget.VerticalAlignment)
.Padding(5.0f,0.0f,0.0f,0.0f)
.AutoWidth()
[
ExtensionWidget.ToSharedRef()
]
];
}
else
{
// create whole row widget, which takes up both the Name and Value columns:
// | Name | Value | Right |
NameColumnBox->SetEnabled(IsEnabledAttribute);
NameColumnBox->AddSlot()
.HAlign(WidgetRow.WholeRowWidget.HorizontalAlignment)
.VAlign(WidgetRow.WholeRowWidget.VerticalAlignment)
.Padding(2.0f,0.0f,0.0f,0.0f)
[
WidgetRow.WholeRowWidget.Widget
];
Splitter->AddSlot()
.Value(ColumnSizeData.GetWholeRowColumnWidth())
.OnSlotResized(ColumnSizeData.GetOnWholeRowColumnResized())
[
NameColumnBox
];
}
TSharedPtr<SWidget> RightmostWidget;
if (GetPropertyNode().IsValid() &&
DetailsView &&
DetailsView->GetDisplayManager().IsValid())
{
TSharedPtr<FDetailsDisplayManager> DisplayManagerLocal = DetailsView->GetDisplayManager();
PRAGMA_DISABLE_DEPRECATION_WARNINGS
if (DisplayManagerLocal->CanConstructPropertyUpdatedWidgetBuilder())
{
DisplayManager = MoveTemp(DisplayManagerLocal);
auto FindParentUObject = [](FPropertyNode* Cursor) -> FObjectPropertyNode*
{
while(Cursor)
{
FObjectPropertyNode* ParentObjectNode = Cursor->AsObjectNode();
if (ParentObjectNode)
{
return ParentObjectNode;
}
else
{
Cursor = Cursor->GetParentNode();
}
}
return nullptr;
};
FObjectPropertyNode* OwningObjectNode = FindParentUObject(Customization->GetPropertyNode().Get());
TArray<TWeakObjectPtr<UObject>> Objects;
if (OwningObjectNode)
{
Objects.Reserve(OwningObjectNode->GetNumObjects());
for(int32 Index = 0, End = OwningObjectNode->GetNumObjects(); Index < End; ++Index)
{
if (UObject* CategoryObject = OwningObjectNode->GetUObject(Index))
{
Objects.Add(CategoryObject);
}
}
}
TSharedPtr<FPropertyNode> PropertyNode = InOwnerTreeNode->GetPropertyNode();
TStrongObjectPtr<UObject> PinnedObject;
if (!Objects.IsEmpty())
{
PinnedObject = Objects[0].Pin();
}
if (PropertyNode && PinnedObject.IsValid())
{
TSharedRef<FPropertyPath> PropertyPath = FPropertyNode::CreatePropertyPath( PropertyNode.ToSharedRef() );
if (PropertyPath->IsValid())
{
FConstructPropertyUpdatedWidgetBuilderArgs Args;
Args.ResetToDefaultAction = FExecuteAction::CreateSP(this, &SDetailSingleItemRow::OnResetToDefaultClicked);
Args.Objects = &Objects;
Args.PropertyPath = PropertyPath;
Args.Category = Category.IsValid() ? Category->GetCategoryName() : NAME_None;
PropertyUpdatedWidgetBuilder = DisplayManager->ConstructPropertyUpdatedWidgetBuilder(Args);
if (PropertyUpdatedWidgetBuilder.IsValid())
{
TAttribute<bool> IsHovered = TAttribute<bool>::CreateSP( this, &SDetailSingleItemRow::IsHovered);
PropertyUpdatedWidgetBuilder->Bind_IsRowHovered(IsHovered);
}
}
}
}
PRAGMA_ENABLE_DEPRECATION_WARNINGS
}
if(PropertyUpdatedWidgetBuilder.IsValid())
{
RightmostWidget = PropertyUpdatedWidgetBuilder->GenerateWidget();
}
// also perform the two additional cases below in case
// PropertyUpdatedWidgetBuilder->GenerateWidget() return nullptr
if(RightmostWidget.IsValid())
{
// nothing to do
}
else if(WidgetRow.HasResetToDefaultContent())
{
RightmostWidget = WidgetRow.ResetToDefaultWidget.Widget;
}
else
{
TArray<FPropertyRowExtensionButton> ExtensionButtons;
UpdateResetToDefault();
FPropertyRowExtensionButton& ResetToDefault = ExtensionButtons.AddDefaulted_GetRef();
ResetToDefault.Label = NSLOCTEXT("PropertyEditor", "ResetToDefault", "Reset to Default");
ResetToDefault.UIAction = FUIAction(
FExecuteAction::CreateSP(this, &SDetailSingleItemRow::OnResetToDefaultClicked),
FCanExecuteAction::CreateLambda([this, IsValueEnabledAttribute]()
{
return IsResetToDefaultVisible() && IsValueEnabledAttribute.Get(true);
})
);
// We could just collapse the Reset to Default button by setting the FIsActionButtonVisible delegate,
// but this would cause the reset to defaults not to reserve space in the toolbar and not be aligned across all rows.
// Instead, we show an empty icon and tooltip and disable the button.
static FSlateIcon EnabledResetToDefaultIcon(FAppStyle::Get().GetStyleSetName(), "PropertyWindow.DiffersFromDefault");
static FSlateIcon DisabledResetToDefaultIcon(FAppStyle::Get().GetStyleSetName(), "NoBrush");
ResetToDefault.Icon = TAttribute<FSlateIcon>::Create([this]()
{
return IsResetToDefaultVisible() ?
EnabledResetToDefaultIcon :
DisabledResetToDefaultIcon;
});
ResetToDefault.ToolTip = TAttribute<FText>::Create([this]()
{
return IsResetToDefaultVisible() ?
NSLOCTEXT("PropertyEditor", "ResetToDefaultPropertyValueToolTip", "Reset this property to its default value.") :
FText::GetEmpty();
});
CreateGlobalExtensionWidgets(ExtensionButtons);
FSlimHorizontalToolBarBuilder ToolbarBuilder(TSharedPtr<FUICommandList>(), FMultiBoxCustomization::None);
ToolbarBuilder.SetLabelVisibility(EVisibility::Collapsed);
ToolbarBuilder.SetStyle(&FAppStyle::Get(), "DetailsView.ExtensionToolBar");
ToolbarBuilder.SetIsFocusable(false);
for (const FPropertyRowExtensionButton& Extension : ExtensionButtons)
{
ToolbarBuilder.AddToolBarButton(Extension.UIAction, NAME_None, Extension.Label, Extension.ToolTip, Extension.Icon);
}
RightmostWidget = ToolbarBuilder.MakeWidget();
}
Splitter->AddSlot()
.Value(ColumnSizeData.GetRightColumnWidth())
.OnSlotResized(ColumnSizeData.GetOnRightColumnResized())
.MinSize(ColumnSizeData.GetRightColumnMinWidth())
[
SNew(SBorder)
.BorderImage(FAppStyle::Get().GetBrush("DetailsView.CategoryMiddle"))
.BorderBackgroundColor(this, &SDetailSingleItemRow::GetOuterBackgroundColor)
.HAlign(HAlign_Right)
.VAlign(VAlign_Center)
.Padding(0.0f)
[
RightmostWidget.ToSharedRef()
]
];
}
}
else
{
// details panel layout became invalid. This is probably a scenario where a widget is coming into view in the parent tree but some external event previous in the frame has invalidated the contents of the details panel.
// The next frame update of the details panel will fix it
Widget = SNew(SSpacer);
}
OwnerTableViewWeak = InOwnerTableView;
auto GetScrollbarWellBrush = [this]()
{
return SDetailTableRowBase::IsScrollBarVisible(OwnerTableViewWeak) ?
FAppStyle::Get().GetBrush("DetailsView.GridLine") :
FAppStyle::Get().GetBrush("DetailsView.CategoryMiddle");
};
auto GetScrollbarWellTint = [this]()
{
return SDetailTableRowBase::IsScrollBarVisible(OwnerTableViewWeak) ?
FStyleColors::White :
this->GetOuterBackgroundColor();
};
auto GetHighlightBorderPadding = [this]()
{
return this->IsHighlighted() ? FMargin(1) : FMargin(0);
};
static const FDetailsViewStyleKey& PrimaryKey = SDetailsView::GetPrimaryDetailsViewStyleKey();
// If this is a stub category with no UProperty data, just show a null widget, we don't have anything useful to show here
if (Category.IsValid() && Category->IsEmpty())
{
this->ChildSlot
[
SNullWidget::NullWidget
];
}
else
{
this->ChildSlot
[
SNew( SBorder )
.BorderImage(FAppStyle::Get().GetBrush("DetailsView.GridLine"))
.Padding(FMargin(0.0f,0.0f,0.0f,1.0f))
.Clipping(EWidgetClipping::ClipToBounds)
[
SNew(SBox)
.MinDesiredHeight(PropertyEditorConstants::PropertyRowHeight)
[
SNew( SHorizontalBox )
+ SHorizontalBox::Slot()
.HAlign(HAlign_Fill)
.VAlign(VAlign_Fill)
[
SNew( SBorder )
.BorderImage(FAppStyle::Get().GetBrush("DetailsView.Highlight"))
.Padding_Lambda(GetHighlightBorderPadding)
[
SNew( SBorder )
.BorderImage(FAppStyle::Get().GetBrush("DetailsView.CategoryMiddle"))
.BorderBackgroundColor(this, &SDetailSingleItemRow::GetOuterBackgroundColor)
.Padding(0.0f)
[
Widget
]
]
]
]
]
];
}
STableRow< TSharedPtr< FDetailTreeNode > >::ConstructInternal(
STableRow::FArguments()
.Style(FAppStyle::Get(), "DetailsView.TreeView.TableRow")
.ShowSelection(false)
.OnDragLeave(DragLeaveDelegate)
.OnAcceptDrop(AcceptDropDelegate)
.OnCanAcceptDrop(CanAcceptDropDelegate),
InOwnerTableView
);
}
FReply SDetailSingleItemRow::OnMouseButtonUp(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent)
{
if (MouseEvent.GetModifierKeys().IsShiftDown())
{
bool bIsHandled = false;
if (CopyAction.CanExecute() && MouseEvent.GetEffectingButton() == EKeys::RightMouseButton)
{
CopyAction.Execute();
PulseAnimation.Play(SharedThis(this));
bIsHandled = true;
}
// Paste is disabled if property editing is disabled
else if (PasteAction.CanExecute() && MouseEvent.GetEffectingButton() == EKeys::LeftMouseButton && OwnerTreeNode.Pin()->GetDetailsViewSharedPtr()->IsPropertyEditingEnabled())
{
PasteAction.Execute();
PulseAnimation.Play(SharedThis(this));
if(TSharedPtr<FPropertyNode> PropertyNode = GetPropertyNode())
{
// Mark property node as animating so we will animate after any potential re-construction
TSharedPtr<IDetailsViewPrivate> DetailsView = OwnerTreeNode.Pin()->GetDetailsViewSharedPtr();
DetailsView->MarkNodeAnimating(PropertyNode, UE::PropertyEditor::Private::PulseAnimationLength);
}
bIsHandled = true;
}
if (bIsHandled)
{
return FReply::Handled();
}
}
else if (MouseEvent.GetModifierKeys().IsControlDown() && GEditor && GWorld)
{
const TSharedPtr<FPropertyNode> PropertyNode = GetPropertyNode();
const TSharedPtr<IPropertyHandle> PropertyHandle = GetPropertyHandle();
if (PropertyNode.IsValid() && PropertyHandle.IsValid())
{
TSharedPtr<IDetailsViewPrivate> DetailsView = OwnerTreeNode.Pin()->GetDetailsViewSharedPtr();
TSharedPtr<IPropertyHandle> Handle = PropertyEditorHelpers::GetPropertyHandle(PropertyNode.ToSharedRef(), DetailsView->GetNotifyHook(), DetailsView->GetPropertyUtilities());
FString Value;
if (Handle->GetValueAsFormattedString(Value, PPF_Copy) == FPropertyAccess::Success)
{
FProperty* Property = PropertyHandle->GetProperty();
TSharedRef<FEditPropertyChain> PropertyChain = PropertyNode->BuildPropertyChain(Property);
check(PropertyChain.IsUnique());
FProperty* TopProperty = PropertyChain->GetHead()->GetValue();
GEditor->SetPropertyColorationTarget(GWorld, Value, Property, TopProperty->GetOwnerClass(), &PropertyChain);
}
}
}
return SDetailTableRowBase::OnMouseButtonUp(MyGeometry, MouseEvent);
}
bool SDetailSingleItemRow::IsResetToDefaultVisible() const
{
return bCachedResetToDefaultVisible;
}
void SDetailSingleItemRow::OnResetToDefaultClicked() const
{
TSharedPtr<IPropertyHandle> PropertyHandle = GetPropertyHandle();
if (WidgetRow.CustomResetToDefault.IsSet())
{
WidgetRow.CustomResetToDefault.GetValue().OnResetToDefaultClicked(PropertyHandle);
}
else if (PropertyHandle.IsValid())
{
TSharedPtr<IPropertyHandleOptional> OptionalHandle = PropertyHandle->AsOptional();
if (!OptionalHandle.IsValid())
{
// This could be the value calling as we share UI between options and their value
OptionalHandle = PropertyHandle->GetParentHandle()->AsOptional();
if (OptionalHandle.IsValid())
{
PropertyHandle = PropertyHandle->GetParentHandle();
}
}
PropertyHandle->ResetToDefault();
}
}
/** Get the background color of the outer part of the row, which contains the edit condition and extension widgets. */
FSlateColor SDetailSingleItemRow::GetOuterBackgroundColor() const
{
if (IsHighlighted() || DragOperation.IsValid())
{
return FAppStyle::Get().GetSlateColor("Colors.Hover");
}
return PropertyEditorConstants::GetRowBackgroundColor(0, this->IsHovered());
}
/** Get the background color of the inner part of the row, which contains the name and value widgets. */
FSlateColor SDetailSingleItemRow::GetInnerBackgroundColor() const
{
FSlateColor Color;
if (IsHighlighted())
{
Color = FAppStyle::Get().GetSlateColor("Colors.Hover");
}
else
{
const int32 IndentLevel = GetIndentLevelForBackgroundColor();
Color = PropertyEditorConstants::GetRowBackgroundColor(IndentLevel, this->IsHovered());
}
if (PulseAnimation.IsPlaying())
{
float Lerp = PulseAnimation.GetLerp();
return FMath::Lerp(FAppStyle::Get().GetSlateColor("Colors.Hover2").GetSpecifiedColor(), Color.GetSpecifiedColor(), Lerp);
}
return Color;
}
void SDetailSingleItemRow::OnCopyGroup()
{
if (!OwnerTreeNode.IsValid())
{
return;
}
if (TArray<TSharedPtr<IPropertyHandle>> GroupProperties = GetPropertyHandles(true);
!GroupProperties.IsEmpty())
{
TArray<FString> PropertiesNotCopied;
PropertiesNotCopied.Reserve(GroupProperties.Num());
TMap<FString, FString> PropertyValues;
PropertyValues.Reserve(GroupProperties.Num());
for (TSharedPtr<IPropertyHandle> PropertyHandle : GroupProperties)
{
if (PropertyHandle.IsValid() && PropertyHandle->IsValidHandle())
{
FString PropertyPath = UE::PropertyEditor::GetPropertyPath(PropertyHandle);
FString PropertyValueStr;
if (PropertyHandle->GetValueAsFormattedString(PropertyValueStr, PPF_Copy) == FPropertyAccess::Success)
{
PropertyValues.Add(PropertyPath, PropertyValueStr);
}
else
{
PropertiesNotCopied.Add(PropertyHandle->GetPropertyDisplayName().ToString());
}
}
}
if (!PropertiesNotCopied.IsEmpty())
{
UE_LOG(
LogPropertyNode,
Warning,
TEXT("One or more of the properties in group \"%s\" was not copied:\n%s"),
*GetRowNameText(),
*FString::Join(PropertiesNotCopied, TEXT("\n")));
}
FPropertyEditorClipboard::ClipboardCopy([&PropertyValues](TMap<FName, FString>& OutTaggedClipboard)
{
for (const TPair<FString, FString>& PropertyValuePair : PropertyValues)
{
OutTaggedClipboard.Add(FName(PropertyValuePair.Key), PropertyValuePair.Value);
}
});
PulseAnimation.Play(SharedThis(this));
}
}
bool SDetailSingleItemRow::CanCopyGroup() const
{
if (!OwnerTreeNode.IsValid())
{
return false;
}
TArray<TSharedPtr<IPropertyHandle>> GroupPropertyHandles = GetPropertyHandles(true);
return !GroupPropertyHandles.IsEmpty();
}
void SDetailSingleItemRow::OnPasteGroup()
{
if (!OwnerTreeNode.IsValid()
|| !CanPasteGroup())
{
return;
}
if (OnPasteFromTextDelegate.IsValid())
{
if (const TArray<TSharedPtr<IPropertyHandle>> GroupProperties = GetPropertyHandles(true);
!GroupProperties.IsEmpty())
{
FScopedTransaction Transaction(NSLOCTEXT("UnrealEd", "PasteGroupProperties", "Paste Group Properties"));
TArray<FString> PropertiesNotPasted;
PropertiesNotPasted.Reserve(GroupProperties.Num());
{
const FGuid OperationGuid = FGuid::NewGuid();
for (const TPair<FName, FString>& PropertyNameValuePair : PreviousClipboardData.PropertyValues)
{
OnPasteFromTextDelegate->Broadcast(PropertyNameValuePair.Key.ToString(), PropertyNameValuePair.Value, OperationGuid);
}
}
if (!PropertiesNotPasted.IsEmpty())
{
UE_LOG(
LogPropertyNode,
Warning,
TEXT("One or more of the properties in group \"%s\" was not pasted:\n%s"),
*GetRowNameText(),
*FString::Join(PropertiesNotPasted, TEXT("\n")));
}
ForceRefresh();
}
}
}
bool SDetailSingleItemRow::CanPasteGroup()
{
if (!OwnerTreeNode.IsValid())
{
return false;
}
const TArray<TSharedPtr<IPropertyHandle>> GroupPropertyHandles = GetPropertyHandles(true);
const TArray<TSharedPtr<FPropertyNode>> GroupPropertyNodes = GetPropertyNodesFromHandles(GroupPropertyHandles);
// @note: We allow pasting to properties that are disabled due to an EditCondition, but not those that are never editable (ie. VisibleAnywhere).
const bool bHasEditables = Algo::AnyOf(GroupPropertyNodes, [](const TSharedPtr<FPropertyNode>& InPropertyNode)
{
constexpr bool bIncludeEditConditionForConstCheck = false;
return !InPropertyNode->IsEditConst(bIncludeEditConditionForConstCheck);
});
// No editable properties to write to
if (!bHasEditables)
{
return false;
}
FString ClipboardContent;
FPropertyEditorClipboard::ClipboardPaste(ClipboardContent);
// If same as last, return previously resolved applicability
if (PreviousClipboardData.Content.Get({}).Equals(ClipboardContent))
{
return PreviousClipboardData.bIsApplicable;
}
// New clipboard contents, non-applicable by default
PreviousClipboardData.Reset();
// Can't be empty, must be json
if (!UE::PropertyEditor::Internal::IsJsonString(ClipboardContent))
{
return false;
}
PreviousClipboardData.Reserve(GroupPropertyHandles.Num());
if (!UE::PropertyEditor::Internal::TryParseClipboard(ClipboardContent, PreviousClipboardData.PropertyValues))
{
return false;
}
PreviousClipboardData.PropertyValues.GenerateKeyArray(PreviousClipboardData.PropertyNames);
TArray<FString> PropertyNames;
Algo::Transform(GroupPropertyHandles, PropertyNames, [](const TSharedPtr<IPropertyHandle>& InPropertyHandle)
{
return UE::PropertyEditor::GetPropertyPath(InPropertyHandle);
});
PreviousClipboardData.PropertyNames.Sort(FNameLexicalLess());
PropertyNames.Sort();
// @note: properties must all match to be applicable
PreviousClipboardData.Content = ClipboardContent;
return PreviousClipboardData.bIsApplicable = Algo::Compare(PreviousClipboardData.PropertyNames, PropertyNames);
}
void SDetailSingleItemRow::PopulateContextMenu(UToolMenu* ToolMenu)
{
SDetailTableRowBase::PopulateContextMenu(ToolMenu);
TSharedPtr<IDetailsViewPrivate> OwningDetailsView;
if (TSharedPtr<FDetailTreeNode> OwnerTreeNodePtr = OwnerTreeNode.Pin())
{
OwningDetailsView = OwnerTreeNodePtr->GetDetailsViewSharedPtr();
}
FToolMenuSection& EditSection = ToolMenu->FindOrAddSection(TEXT("Edit"));
{
if (CopyAction.IsBound() && PasteAction.IsBound())
{
constexpr bool bLongDisplayName = false;
const bool bIsGroup = Customization->IsValidCustomization() && Customization->DetailGroup.IsValid();
// Copy
{
TAttribute<FText> Label;
TAttribute<FText> ToolTip;
if (bIsGroup)
{
Label = NSLOCTEXT("PropertyView", "CopyGroupProperties", "Copy All Properties in Group");
ToolTip = TAttribute<FText>::CreateLambda([this]()
{
return CanCopyGroup()
? NSLOCTEXT("PropertyView", "CopyGroupProperties_ToolTip", "Copy all properties in this group")
: NSLOCTEXT("PropertyView", "CantCopyGroupProperties_ToolTip", "None of the properties in this group can be copied");
});
}
else
{
Label = NSLOCTEXT("PropertyView", "CopyProperty", "Copy");
ToolTip = NSLOCTEXT("PropertyView", "CopyProperty_ToolTip", "Copy this property value");
}
FToolMenuEntry& CopyMenuEntry = EditSection.AddMenuEntry(
TEXT("Copy"),
Label,
ToolTip,
FSlateIcon(FCoreStyle::Get().GetStyleSetName(), "GenericCommands.Copy"),
CopyAction);
CopyMenuEntry.InputBindingLabel = FInputChord(EModifierKey::Shift, EKeys::RightMouseButton).GetInputText(bLongDisplayName);
}
// Paste
{
// Paste is only enabled if property editing is enabled
if (OwningDetailsView && OwningDetailsView->IsPropertyEditingEnabled() && WidgetRow.EditConditionValue.Get(true /*DefaultValue*/))
{
TAttribute<FText> Label;
TAttribute<FText> ToolTip;
if (bIsGroup)
{
Label = NSLOCTEXT("PropertyView", "PasteGroupProperties", "Paste All Properties in Group");
ToolTip = TAttribute<FText>::CreateLambda([this]()
{
return CanPasteGroup()
? NSLOCTEXT("PropertyView", "PasteGroupProperties_ToolTip", "Paste the copied property values here")
// @note: this is specific to the constraint that the destination group has to match the source group (copied from) exactly
: NSLOCTEXT("PropertyView", "CantPasteGroupProperties_ToolTip", "The properties in this group don't match the contents of the clipboard, or the properties aren't editable");
});
}
else
{
Label = NSLOCTEXT("PropertyView", "PasteProperty", "Paste");
ToolTip = NSLOCTEXT("PropertyView", "PasteProperty_ToolTip", "Paste the copied value here");
}
FToolMenuEntry& PasteMenuEntry = EditSection.AddMenuEntry(
TEXT("Paste"),
Label,
ToolTip,
FSlateIcon(FCoreStyle::Get().GetStyleSetName(), "GenericCommands.Paste"),
PasteAction);
PasteMenuEntry.InputBindingLabel = FInputChord(EModifierKey::Shift, EKeys::LeftMouseButton).GetInputText(bLongDisplayName);
}
}
}
// Copy Display Name
{
FUIAction CopyDisplayNameAction = FExecuteAction::CreateSP(this, &SDetailSingleItemRow::OnCopyPropertyDisplayName);
CopyDisplayNameAction.CanExecuteAction = FCanExecuteAction::CreateSP(this, &SDetailSingleItemRow::CanCopyPropertyDisplayName);
static const FTextFormat TooltipFormat = NSLOCTEXT("PropertyView_Single", "CopyPropertyDisplayName_ToolTip", "Copy the display name of this property to the system clipboard:\n{0}");
EditSection.AddMenuEntry(
TEXT("CopyDisplayName"),
NSLOCTEXT("PropertyView", "CopyPropertyDisplayName", "Copy Display Name"),
FText::Format(TooltipFormat, GetPropertyDisplayName()),
FSlateIcon(FCoreStyle::Get().GetStyleSetName(), "GenericCommands.Copy"),
CopyDisplayNameAction);
}
// Copy Internal Name
{
FUIAction CopyInternalNameAction = FExecuteAction::CreateSP(this, &SDetailSingleItemRow::OnCopyPropertyInternalName);
CopyInternalNameAction.CanExecuteAction = FCanExecuteAction::CreateSP(this, &SDetailSingleItemRow::CanCopyPropertyInternalName);
static const FTextFormat TooltipFormat = NSLOCTEXT("PropertyView_Single", "CopyPropertyInternalName_ToolTip", "Copy the internal name of this property to the system clipboard:\n{0}");
EditSection.AddMenuEntry(
TEXT("CopyInternalName"),
NSLOCTEXT("PropertyView", "CopyPropertyInternalName", "Copy Internal Name"),
FText::Format(TooltipFormat, FText::FromString(GetPropertyInternalName())),
FSlateIcon(FCoreStyle::Get().GetStyleSetName(), "GenericCommands.Copy"),
CopyInternalNameAction);
}
// Favorite
{
if (OwnerTreeNode.Pin()->GetDetailsViewSharedPtr()->IsFavoritingEnabled())
{
FUIAction FavoriteAction;
FavoriteAction.ExecuteAction = FExecuteAction::CreateSP(this, &SDetailSingleItemRow::OnFavoriteMenuToggle);
FavoriteAction.CanExecuteAction = FCanExecuteAction::CreateSP(this, &SDetailSingleItemRow::CanFavorite);
FText FavoriteText = NSLOCTEXT("PropertyView", "FavoriteProperty", "Add to Favorites");
FText FavoriteTooltipText = NSLOCTEXT("PropertyView", "FavoriteProperty_ToolTip", "Add this property to your favorites.");
FName FavoriteIcon = "DetailsView.PropertyIsFavorite";
if (IsFavorite())
{
FavoriteText = NSLOCTEXT("PropertyView", "RemoveFavoriteProperty", "Remove from Favorites");
FavoriteTooltipText = NSLOCTEXT("PropertyView", "RemoveFavoriteProperty_ToolTip", "Remove this property from your favorites.");
FavoriteIcon = "DetailsView.PropertyIsNotFavorite";
}
EditSection.AddMenuEntry(
TEXT("ToggleFavorite"),
FavoriteText,
FavoriteTooltipText,
FSlateIcon(FAppStyle::Get().GetStyleSetName(), FavoriteIcon),
FavoriteAction);
}
}
if (FPropertyEditorPermissionList::Get().ShouldShowMenuEntries())
{
// Hide separator line if it only contains the SearchWidget, making the next 2 elements the top of the list
if (EditSection.Blocks.Num() > 1)
{
EditSection.AddSeparator(NAME_None);
}
EditSection.AddMenuEntry(
TEXT("CopyRowName"),
NSLOCTEXT("PropertyView", "CopyRowName", "Copy internal row name"),
NSLOCTEXT("PropertyView", "CopyRowName_ToolTip", "Copy the row's parent struct and internal name to use in the property editor's allow/deny lists."),
FSlateIcon(),
FUIAction(FExecuteAction::CreateSP(this, &SDetailSingleItemRow::CopyRowNameText)));
EditSection.AddMenuEntry(
TEXT("AddAllowList"),
NSLOCTEXT("PropertyView", "AddAllowList", "Add to Allowed"),
NSLOCTEXT("PropertyView", "AddAllowList_ToolTip", "Add this row to the property editor's allowed properties list."),
FSlateIcon(),
FUIAction(
FExecuteAction::CreateSP(this, &SDetailSingleItemRow::OnToggleAllowList),
FCanExecuteAction(),
FIsActionChecked::CreateSP(this, &SDetailSingleItemRow::IsAllowListChecked)),
EUserInterfaceActionType::Check,
NAME_None);
EditSection.AddMenuEntry(
TEXT("AddDenyList"),
NSLOCTEXT("PropertyView", "AddDenyList", "Add to Denied"),
NSLOCTEXT("PropertyView", "AddDenyList_ToolTip", "Add this row to the property editor's denied properties list."),
FSlateIcon(),
FUIAction(
FExecuteAction::CreateSP(this, &SDetailSingleItemRow::OnToggleDenyList),
FCanExecuteAction(),
FIsActionChecked::CreateSP(this, &SDetailSingleItemRow::IsDenyListChecked)),
EUserInterfaceActionType::Check,
NAME_None);
}
if (WidgetRow.CustomMenuItems.Num() > 0)
{
// Hide separator line if it only contains the SearchWidget, making the next 2 elements the top of the list
if (EditSection.Blocks.Num() > 1)
{
EditSection.AddSeparator(NAME_None);
}
for (const FDetailWidgetRow::FCustomMenuData& CustomMenuData : WidgetRow.CustomMenuItems)
{
// Add the menu entry
EditSection.AddMenuEntry(
CustomMenuData.GetEntryName(),
CustomMenuData.Name,
CustomMenuData.Tooltip,
CustomMenuData.SlateIcon,
CustomMenuData.Action);
}
}
}
}
TArray<TSharedPtr<IPropertyHandle>> SDetailSingleItemRow::GetPropertyHandles(const bool bRecursive) const
{
if (TArray<TSharedPtr<IPropertyHandle>> PropertyHandles = SDetailTableRowBase::GetPropertyHandles(bRecursive);
!PropertyHandles.IsEmpty())
{
return PropertyHandles;
}
return WidgetRow.GetPropertyHandles();
}
TSharedPtr<IPropertyHandle> SDetailSingleItemRow::GetPrimaryPropertyHandle() const
{
TSharedPtr<IPropertyHandle> PrimaryPropertyHandle = SDetailTableRowBase::GetPrimaryPropertyHandle();
if (PrimaryPropertyHandle.IsValid())
{
return PrimaryPropertyHandle;
}
if (const TArray<TSharedPtr<IPropertyHandle>>& WidgetPropertyHandles = WidgetRow.GetPropertyHandles();
!WidgetPropertyHandles.IsEmpty())
{
return WidgetPropertyHandles[0];
}
return nullptr;
}
void SDetailSingleItemRow::OnCopyProperty()
{
if (OwnerTreeNode.IsValid())
{
TSharedPtr<FPropertyNode> PropertyNode = GetPropertyNode();
if (PropertyNode.IsValid())
{
TSharedPtr<IDetailsViewPrivate> DetailsView = OwnerTreeNode.Pin()->GetDetailsViewSharedPtr();
TSharedPtr<IPropertyHandle> Handle = PropertyEditorHelpers::GetPropertyHandle(PropertyNode.ToSharedRef(), DetailsView->GetNotifyHook(), DetailsView->GetPropertyUtilities());
FString Value;
if (Handle->GetValueAsFormattedString(Value, PPF_Copy) == FPropertyAccess::Success)
{
FPropertyEditorClipboard::ClipboardCopy(*Value);
PulseAnimation.Play(SharedThis(this));
}
}
}
}
FText SDetailSingleItemRow::GetPropertyDisplayName() const
{
if (!OwnerTreeNode.IsValid())
{
return { };
}
const TSharedPtr<FPropertyNode> PropertyNode = GetPropertyNode();
if (!PropertyNode.IsValid())
{
return { };
}
if (PropertyEditorHelpers::IsChildOfOption(*PropertyNode))
{
return PropertyNode->GetParentNode()->GetDisplayName();
}
return PropertyNode->GetDisplayName();
}
void SDetailSingleItemRow::OnCopyPropertyDisplayName()
{
FPropertyEditorClipboard::ClipboardCopy(*GetPropertyDisplayName().ToString());
}
bool SDetailSingleItemRow::CanCopyPropertyDisplayName()
{
return !GetPropertyDisplayName().IsEmpty();
}
FString SDetailSingleItemRow::GetPropertyInternalName() const
{
if (!OwnerTreeNode.IsValid())
{
return { };
}
const TSharedPtr<FPropertyNode> PropertyNode = GetPropertyNode();
if (!PropertyNode.IsValid())
{
return { };
}
const FProperty* Property = PropertyEditorHelpers::IsChildOfOption(*PropertyNode) ? PropertyEditorHelpers::GetOptionParent(*PropertyNode) : PropertyNode->GetProperty();
if (!Property)
{
return { };
}
const UStruct* OwnerStruct = Property->GetOwnerStruct();
if (!OwnerStruct)
{
return { };
}
return OwnerStruct->GetAuthoredNameForField(Property);
}
void SDetailSingleItemRow::OnCopyPropertyInternalName()
{
const FString InternalName = GetPropertyInternalName();
if (!InternalName.IsEmpty())
{
FPropertyEditorClipboard::ClipboardCopy(*InternalName);
}
}
bool SDetailSingleItemRow::CanCopyPropertyInternalName()
{
return !GetPropertyInternalName().IsEmpty();
}
bool SDetailSingleItemRow::CanPasteProperty() const
{
TSharedPtr<FPropertyNode> PropertyNode = GetPropertyNode();
check(PropertyNode.IsValid());
// Check if the property is editable first (a failed EditCondition is still considered to be Editable, to make copying to PostProcess settings etc. practical)
constexpr bool bIncludeEditConditionForConstCheck = false;
if (PropertyNode->IsEditConst(bIncludeEditConditionForConstCheck)) // Ignore EditCondition state
{
return false;
}
FString ClipboardContent;
FPropertyEditorClipboard::ClipboardPaste(ClipboardContent);
return CanPasteFromText(TEXT(""), ClipboardContent);
}
TSharedRef<SWidget> SDetailSingleItemRow::GetNameWidget(TSharedRef<SWidget> NameWidget, const TSharedPtr<FPropertyNode>& Node) const
{
if (Node.IsValid() && OwnerTreeNode.IsValid() )
{
const TSharedPtr<FDetailTreeNode> DetailTreeNodeSP = OwnerTreeNode.Pin();
if (DetailTreeNodeSP.IsValid())
{
if (TSharedPtr<IDetailsViewPrivate> DetailsView = DetailTreeNodeSP->GetDetailsViewSharedPtr())
{
const TSharedPtr<FDetailsNameWidgetOverrideCustomization> DetailsNameWidgetOverrideCustomization =
DetailsView->GetDetailsNameWidgetOverrideCustomization();
if (DetailsNameWidgetOverrideCustomization.IsValid())
{
const TSharedRef< FPropertyPath > Path = FPropertyNode::CreatePropertyPath(Node.ToSharedRef());
return DetailsNameWidgetOverrideCustomization->CustomizeName(NameWidget, Path.Get());
}
}
}
}
return NameWidget;
}
bool SDetailSingleItemRow::CanPasteFromText(const FString& InTag, const FString& InText) const
{
if (InText.IsEmpty())
{
return false;
}
const TSharedPtr<FPropertyNode> PropertyNode = GetPropertyNode();
// Prevent paste if we cannot find the property node to paste into.
if (!PropertyNode.IsValid())
{
return false;
}
const TSharedPtr<IPropertyHandle> PropertyHandle = GetPropertyHandle();
// We won't be able to paste without a property handle.
if (!PropertyHandle.IsValid())
{
return false;
}
if (const bool bIsTagged = !InTag.IsEmpty();
bIsTagged)
{
const FString PropertyPath = UE::PropertyEditor::Private::GetPropertyPath(
[&]() { return GetPropertyHandle(); },
[&]() { return PropertyNode; });
// Ensure that if tag is specified, that it matches the subscriber.
if (!InTag.Equals(PropertyPath))
{
return false;
}
}
// Prevent paste from working if the property's edit condition is not met.
// Allow paste if no property row can be found.
{
TSharedPtr<FDetailPropertyRow> PropertyRow = Customization->PropertyRow;
if (!PropertyRow.IsValid() && Customization->DetailGroup.IsValid())
{
PropertyRow = Customization->DetailGroup->GetHeaderPropertyRow();
}
if (PropertyRow.IsValid())
{
if (const FPropertyEditor* PropertyEditor = PropertyRow->GetPropertyEditor().Get())
{
return !PropertyEditor->IsEditConst();
}
}
}
return true;
}
void SDetailSingleItemRow::OnPasteProperty()
{
FString ClipboardContent;
const TSharedPtr<FPropertyNode> PropertyNode = GetPropertyNode();
const FString PropertyPath = UE::PropertyEditor::Private::GetPropertyPath(
[&]() { return GetPropertyHandle(); },
[&]() { return PropertyNode; });
if (PropertyPath.IsEmpty())
{
FPropertyEditorClipboard::ClipboardPaste(ClipboardContent);
}
else
{
FPropertyEditorClipboard::ClipboardPaste(ClipboardContent, FName(PropertyPath));
}
if (PasteFromText(TEXT(""), ClipboardContent))
{
// Mark property node as animating so we will animate after re-construction
TSharedPtr<IDetailsViewPrivate> DetailsView = OwnerTreeNode.Pin()->GetDetailsViewSharedPtr();
DetailsView->MarkNodeAnimating(PropertyNode, UE::PropertyEditor::Private::PulseAnimationLength);
// Need to refresh the details panel in case a property was pasted over another.
ForceRefresh();
}
}
void SDetailSingleItemRow::OnPasteFromText(const FString& InTag, const FString& InText, const TOptional<FGuid>& InOperationId)
{
if (PasteFromText(InTag, InText))
{
TSharedPtr<FPropertyNode> PropertyNode = GetPropertyNode();
if (PropertyNode.IsValid())
{
// Mark property node as animating so we will animate after re-construction
TSharedPtr<IDetailsViewPrivate> DetailsView = OwnerTreeNode.Pin()->GetDetailsViewSharedPtr();
DetailsView->MarkNodeAnimating(PropertyNode, UE::PropertyEditor::Private::PulseAnimationLength, InOperationId);
}
}
}
bool SDetailSingleItemRow::PasteFromText(const FString& InTag, const FString& InText)
{
if (!CanPasteFromText(InTag, InText))
{
return false;
}
// The logic below is largely taken from SDisplayClusterColorGradingColorWheel::CommitColor,
// which avoids writing to trashed objects
FScopedTransaction Transaction(NSLOCTEXT("UnrealEd", "PasteProperty", "Paste Property"));
EPropertyValueSetFlags::Type PropertyValueSetFlags = EPropertyValueSetFlags::InstanceObjects;
const bool bIsTagged = !InTag.IsEmpty();
// If tagged, skip individual property transactions. Instead, a single undo will revert all changes in the batch paste.
// @todo: would be better to indicate that this is a batched paste rather than checking for a tag
if (bIsTagged)
{
PropertyValueSetFlags |= EPropertyValueSetFlags::NotTransactable;
}
const TSharedPtr<IPropertyHandle> PropertyHandle = GetPropertyHandle();
if (!PropertyHandle.IsValid())
{
return false;
}
if (PropertyHandle->SetValueFromFormattedString(InText, PropertyValueSetFlags) != FPropertyAccess::Success)
{
return false;
}
if (bIsTagged)
{
TArray<UObject*> OuterObjects;
PropertyHandle->GetOuterObjects(OuterObjects);
for (UObject* Object : OuterObjects)
{
if (!Object->HasAnyFlags(RF_Transactional))
{
Object->SetFlags(RF_Transactional);
}
SaveToTransactionBuffer(Object, false);
SnapshotTransactionBuffer(Object);
}
}
return true;
}
// Helper function to determine which parent Struct a property comes from. If PropertyName is not a real property,
// it will return the passed-in Struct (eg a DetailsCustomization with a custom name is a "fake" property).
const UStruct* GetExactStructForProperty(const UStruct* MostDerivedStruct, const FName PropertyName)
{
if (FProperty* Property = MostDerivedStruct->FindPropertyByName(PropertyName))
{
return Property->GetOwnerStruct();
}
return MostDerivedStruct;
}
FString SDetailSingleItemRow::GetRowNameText() const
{
if (const TSharedPtr<FDetailTreeNode> Owner = OwnerTreeNode.Pin())
{
const UStruct* BaseStructure = Owner->GetParentBaseStructure();
if (BaseStructure)
{
const UStruct* ExactStruct = GetExactStructForProperty(Owner->GetParentBaseStructure(), Owner->GetNodeName());
return FString::Printf(TEXT("(%s, %s)"), *FSoftObjectPtr(ExactStruct).ToString(), *Owner->GetNodeName().ToString());
}
}
return FString();
}
void SDetailSingleItemRow::CopyRowNameText() const
{
const FString RowNameText = GetRowNameText();
if (!RowNameText.IsEmpty())
{
FPropertyEditorClipboard::ClipboardCopy(*RowNameText);
}
}
void SDetailSingleItemRow::OnToggleAllowList() const
{
const TSharedPtr<FDetailTreeNode> Owner = OwnerTreeNode.Pin();
if (Owner)
{
const FName OwnerName = "DetailRowContextMenu";
const UStruct* ExactStruct = GetExactStructForProperty(Owner->GetParentBaseStructure(), Owner->GetNodeName());
if (IsAllowListChecked())
{
FPropertyEditorPermissionList::Get().RemoveFromAllowList(ExactStruct, Owner->GetNodeName(), OwnerName);
UE_LOG(LogPropertyEditorPermissionList, Log, TEXT("Removing %s from AllowList"), *GetRowNameText());
}
else
{
FPropertyEditorPermissionList::Get().AddToAllowList(ExactStruct, Owner->GetNodeName(), OwnerName);
UE_LOG(LogPropertyEditorPermissionList, Log, TEXT("Adding %s to AllowList"), *GetRowNameText());
}
}
}
bool SDetailSingleItemRow::IsAllowListChecked() const
{
if (const TSharedPtr<FDetailTreeNode> Owner = OwnerTreeNode.Pin())
{
const UStruct* ExactStruct = GetExactStructForProperty(Owner->GetParentBaseStructure(), Owner->GetNodeName());
return FPropertyEditorPermissionList::Get().IsSpecificPropertyAllowListed(ExactStruct, Owner->GetNodeName());
}
return false;
}
void SDetailSingleItemRow::OnToggleDenyList() const
{
const TSharedPtr<FDetailTreeNode> Owner = OwnerTreeNode.Pin();
if (Owner)
{
const FName OwnerName = "DetailRowContextMenu";
const UStruct* ExactStruct = GetExactStructForProperty(Owner->GetParentBaseStructure(), Owner->GetNodeName());
if (IsDenyListChecked())
{
FPropertyEditorPermissionList::Get().RemoveFromDenyList(ExactStruct, Owner->GetNodeName(), OwnerName);
UE_LOG(LogPropertyEditorPermissionList, Log, TEXT("Removing %s from DenyList"), *GetRowNameText());
}
else
{
FPropertyEditorPermissionList::Get().AddToDenyList(ExactStruct, Owner->GetNodeName(), OwnerName);
UE_LOG(LogPropertyEditorPermissionList, Log, TEXT("Adding %s to DenyList"), *GetRowNameText());
}
}
}
bool SDetailSingleItemRow::IsDenyListChecked() const
{
if (const TSharedPtr<FDetailTreeNode> Owner = OwnerTreeNode.Pin())
{
return FPropertyEditorPermissionList::Get().IsSpecificPropertyDenyListed(Owner->GetParentBaseStructure(), Owner->GetNodeName());
}
return false;
}
void SDetailSingleItemRow::PopulateExtensionWidget()
{
TSharedPtr<FDetailTreeNode> OwnerTreeNodePinned = OwnerTreeNode.Pin();
if (OwnerTreeNodePinned.IsValid())
{
TSharedPtr<IDetailsViewPrivate> DetailsView = OwnerTreeNodePinned->GetDetailsViewSharedPtr();
TSharedPtr<IDetailPropertyExtensionHandler> ExtensionHandler = DetailsView->GetExtensionHandler();
if (Customization->HasPropertyNode() && ExtensionHandler.IsValid())
{
TSharedPtr<IPropertyHandle> Handle = PropertyEditorHelpers::GetPropertyHandle(Customization->GetPropertyNode().ToSharedRef(), nullptr, nullptr);
const UClass* ObjectClass = Handle->GetOuterBaseClass();
if (Handle->IsValidHandle() && ExtensionHandler->IsPropertyExtendable(ObjectClass, *Handle))
{
IDetailLayoutBuilder& DetailLayout = OwnerTreeNodePinned->GetParentCategory()->GetParentLayout();
ExtensionHandler->ExtendWidgetRow(WidgetRow, DetailLayout, ObjectClass, Handle);
}
}
}
}
bool SDetailSingleItemRow::CanFavorite() const
{
if (Customization->HasPropertyNode())
{
return true;
}
if (Customization->HasCustomBuilder())
{
TSharedPtr<IPropertyHandle> PropertyHandle = Customization->CustomBuilderRow->GetPropertyHandle();
const FString& OriginalPath = Customization->CustomBuilderRow->GetOriginalPath();
return PropertyHandle.IsValid() || !OriginalPath.IsEmpty();
}
return false;
}
bool SDetailSingleItemRow::IsFavorite() const
{
if (Customization->HasPropertyNode())
{
return Customization->GetPropertyNode()->IsFavorite();
}
if (Customization->HasCustomBuilder())
{
TSharedPtr<FDetailTreeNode> OwnerTreeNodePinned = OwnerTreeNode.Pin();
if (OwnerTreeNodePinned.IsValid())
{
TSharedPtr<FDetailCategoryImpl> ParentCategory = OwnerTreeNodePinned->GetParentCategory();
if (ParentCategory.IsValid() && ParentCategory->IsFavoriteCategory())
{
return true;
}
TSharedPtr<IPropertyHandle> PropertyHandle = Customization->CustomBuilderRow->GetPropertyHandle();
if (PropertyHandle.IsValid())
{
return PropertyHandle->IsFavorite();
}
const FString& OriginalPath = Customization->CustomBuilderRow->GetOriginalPath();
if (!OriginalPath.IsEmpty())
{
return OwnerTreeNodePinned->GetDetailsViewSharedPtr()->IsCustomBuilderFavorite(OriginalPath);
}
}
}
return false;
}
void SDetailSingleItemRow::OnFavoriteMenuToggle()
{
if (!CanFavorite())
{
return;
}
TSharedPtr<FDetailTreeNode> OwnerTreeNodePinned = OwnerTreeNode.Pin();
if (!OwnerTreeNodePinned.IsValid())
{
return;
}
TSharedPtr<IDetailsViewPrivate> DetailsView = OwnerTreeNodePinned->GetDetailsViewSharedPtr();
bool bNewValue = !IsFavorite();
if (Customization->HasPropertyNode())
{
TSharedPtr<FPropertyNode> PropertyNode = Customization->GetPropertyNode();
PropertyNode->SetFavorite(bNewValue);
}
else if (Customization->HasCustomBuilder())
{
TSharedPtr<IPropertyHandle> PropertyHandle = Customization->CustomBuilderRow->GetPropertyHandle();
const FString& OriginalPath = Customization->CustomBuilderRow->GetOriginalPath();
if (PropertyHandle.IsValid())
{
StaticCastSharedPtr<FPropertyHandleBase>(PropertyHandle)->GetPropertyNode()->SetFavorite(bNewValue);
}
else if (!OriginalPath.IsEmpty())
{
bNewValue = !DetailsView->IsCustomBuilderFavorite(OriginalPath);
DetailsView->SetCustomBuilderFavorite(OriginalPath, bNewValue);
}
}
// Calculate the scrolling offset (by item) to make sure the mouse stay over the same property
int32 ExpandSize = 0;
if (OwnerTreeNodePinned->ShouldBeExpanded())
{
SDetailSingleItemRow_Helper::RecursivelyGetItemShow(OwnerTreeNodePinned.ToSharedRef(), ExpandSize);
}
else
{
// if the item is not expand count is 1
ExpandSize = 1;
}
// Apply the calculated offset
DetailsView->MoveScrollOffset(bNewValue ? ExpandSize : -ExpandSize);
// Refresh the tree
ForceRefresh();
}
void SDetailSingleItemRow::CreateGlobalExtensionWidgets(TArray<FPropertyRowExtensionButton>& OutExtensions) const
{
// fetch global extension widgets
FPropertyEditorModule& PropertyEditorModule = FModuleManager::Get().GetModuleChecked<FPropertyEditorModule>("PropertyEditor");
FOnGenerateGlobalRowExtensionArgs Args;
Args.OwnerTreeNode = OwnerTreeNode;
if (Customization->HasPropertyNode())
{
Args.PropertyHandle = PropertyEditorHelpers::GetPropertyHandle(Customization->GetPropertyNode().ToSharedRef(), nullptr, nullptr);
}
else if (const TSharedPtr<IPropertyHandle> PrimaryPropertyHandle = GetPrimaryPropertyHandle();
PrimaryPropertyHandle.IsValid() && PrimaryPropertyHandle->IsValidHandle())
{
Args.PropertyHandle = PrimaryPropertyHandle;
}
PropertyEditorModule.GetGlobalRowExtensionDelegate().Broadcast(Args, OutExtensions);
}
bool SDetailSingleItemRow::IsHighlighted() const
{
TSharedPtr<FDetailTreeNode> OwnerTreeNodePtr = OwnerTreeNode.Pin();
return OwnerTreeNodePtr.IsValid() ? OwnerTreeNodePtr->IsHighlighted() : false;
}
TSharedPtr<FDragDropOperation> SDetailSingleItemRow::CreateDragDropOperation()
{
if (WidgetRow.CustomDragDropHandler)
{
TSharedPtr<FDragDropOperation> DragOp = WidgetRow.CustomDragDropHandler->CreateDragDropOperation();
DragOperation = DragOp;
return DragOp;
}
else
{
TSharedPtr<FArrayRowDragDropOp> ArrayDragOp = MakeShareable(new FArrayRowDragDropOp(SharedThis(this)));
ArrayDragOp->Init();
DragOperation = ArrayDragOp;
return ArrayDragOp;
}
}
void SDetailSingleItemRow::Tick(const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime)
{
UpdateResetToDefault();
}
void SArrayRowHandle::Construct(const FArguments& InArgs)
{
ParentRow = InArgs._ParentRow;
ChildSlot
[
InArgs._Content.Widget
];
}
FReply SArrayRowHandle::OnDragDetected(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent)
{
if (MouseEvent.IsMouseButtonDown(EKeys::LeftMouseButton))
{
TSharedPtr<FDragDropOperation> DragDropOp = ParentRow.Pin()->CreateDragDropOperation();
if (DragDropOp.IsValid())
{
return FReply::Handled().BeginDragDrop(DragDropOp.ToSharedRef());
}
}
return FReply::Unhandled();
}
FArrayRowDragDropOp::FArrayRowDragDropOp(TSharedPtr<SDetailSingleItemRow> InRow)
{
check(InRow.IsValid());
Row = InRow;
MouseCursor = EMouseCursor::GrabHandClosed;
}
void FArrayRowDragDropOp::Init()
{
SetValidTarget(false);
SetupDefaults();
Construct();
}
void FArrayRowDragDropOp::SetValidTarget(bool IsValidTarget)
{
if (IsValidTarget)
{
CurrentHoverText = NSLOCTEXT("ArrayDragDrop", "PlaceRowHere", "Place Row Here");
CurrentIconBrush = FAppStyle::GetBrush("Graph.ConnectorFeedback.OK");
}
else
{
CurrentHoverText = NSLOCTEXT("ArrayDragDrop", "CannotPlaceRowHere", "Cannot Place Row Here");
CurrentIconBrush = FAppStyle::GetBrush("Graph.ConnectorFeedback.Error");
}
}