// 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 ParentItem, int32& ItemShowNum) { if (ParentItem->GetVisibility() == ENodeVisibility::Visible) { ItemShowNum++; } if (ParentItem->ShouldBeExpanded()) { TArray< TSharedRef > Childrens; ParentItem->GetChildren(Childrens); for (TSharedRef ItemChild : Childrens) { RecursivelyGetItemShow(ItemChild, ItemShowNum); } } } } void SDetailSingleItemRow::OnArrayOrCustomDragLeave(const FDragDropEvent& DragDropEvent) { TSharedPtr DecoratedOp = DragDropEvent.GetOperationAs(); 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 RowPtr, EItemDropZone DropZone) const { // Can't drop onto another array item; need to drop above or below if (DropZone == EItemDropZone::OntoItem) { return false; } TSharedPtr 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 DetailsView = OwnerTreeNode.Pin()->GetDetailsViewSharedPtr(); TSharedPtr SwappingHandle = PropertyEditorHelpers::GetPropertyHandle(SwappingPropertyNode.ToSharedRef(), DetailsView->GetNotifyHook(), DetailsView->GetPropertyUtilities()); TSharedPtr ParentHandle = SwappingHandle->GetParentHandle()->AsArray(); if (ParentHandle.IsValid() && SwappablePropertyNode->GetParentNode() == SwappingPropertyNode->GetParentNode()) { return true; } } } return false; } FReply SDetailSingleItemRow::OnArrayAcceptDrop(const FDragDropEvent& DragDropEvent, EItemDropZone DropZone, TSharedPtr TargetItem) { TSharedPtr ArrayDropOp = DragDropEvent.GetOperationAs< FArrayRowDragDropOp >(); if (!ArrayDropOp.IsValid()) { return FReply::Unhandled(); } TSharedPtr RowPtr = ArrayDropOp->Row.Pin(); if (!RowPtr.IsValid()) { return FReply::Unhandled(); } if (!CheckValidDrop(RowPtr, DropZone)) { return FReply::Unhandled(); } TSharedPtr DetailsView = OwnerTreeNode.Pin()->GetDetailsViewSharedPtr(); TSharedPtr SwappingPropertyNode = RowPtr->SwappablePropertyNode; TSharedPtr SwappingHandle = PropertyEditorHelpers::GetPropertyHandle(SwappingPropertyNode.ToSharedRef(), DetailsView->GetNotifyHook(), DetailsView->GetPropertyUtilities()); TSharedPtr 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 SDetailSingleItemRow::OnArrayCanAcceptDrop(const FDragDropEvent& DragDropEvent, EItemDropZone DropZone, TSharedPtr TargetItem) { TSharedPtr ArrayDropOp = DragDropEvent.GetOperationAs< FArrayRowDragDropOp >(); if (!ArrayDropOp.IsValid()) { return TOptional(); } TSharedPtr RowPtr = ArrayDropOp->Row.Pin(); if (!RowPtr.IsValid()) { return TOptional(); } // 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(); } 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 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 SDetailSingleItemRow::OnCustomCanAcceptDrop(const FDragDropEvent& DragDropEvent, EItemDropZone DropZone, TSharedPtr 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(); } TSharedPtr SDetailSingleItemRow::GetPropertyNode() const { TSharedPtr 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 PropertyHandle = Customization->CustomBuilderRow->GetPropertyHandle(); if (PropertyHandle.IsValid()) { PropertyNode = StaticCastSharedPtr(PropertyHandle)->GetPropertyNode(); } } return PropertyNode; } TSharedPtr SDetailSingleItemRow::GetPropertyHandle() const { TSharedPtr Handle; if (const TSharedPtr PropertyNode = GetPropertyNode()) { if (const TSharedPtr OwnerTreeNodePtr = OwnerTreeNode.Pin()) { if (TSharedPtr 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 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 InOwnerTreeNode, const TSharedRef& InOwnerTableView ) { OwnerTreeNode = InOwnerTreeNode; bAllowFavoriteSystem = InArgs._AllowFavoriteSystem; Customization = InCustomization; TSharedRef Widget = SNullWidget::NullWidget; FOnTableRowDragLeave DragLeaveDelegate; FOnAcceptDrop AcceptDropDelegate; FOnCanAcceptDrop CanAcceptDropDelegate; TSharedPtr 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 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 Category = InOwnerTreeNode->GetParentCategory(); const bool bIsValidTreeNode = InOwnerTreeNode->GetParentCategory().IsValid() && InOwnerTreeNode->GetParentCategory()->IsParentLayoutValid(); if (bIsValidTreeNode) { if (Customization->IsValidCustomization()) { TSharedPtr 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 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 GroupOnPasteFromTextDelegate = Group->OnPasteFromText()) { OnPasteFromTextDelegate = GroupOnPasteFromTextDelegate; } } if (OnPasteFromTextDelegate.IsValid()) { OnPasteFromTextDelegate->AddSP(this, &SDetailSingleItemRow::OnPasteFromText); } } TSharedPtr NameWidget = WidgetRow.NameWidget.Widget; TSharedPtr ValueWidget = SNew(SConstrainedBox) .MinWidth(WidgetRow.ValueWidget.MinWidth) .MaxWidth(WidgetRow.ValueWidget.MaxWidth) [ WidgetRow.ValueWidget.Widget ]; TSharedPtr ExtensionWidget = WidgetRow.ExtensionWidget.Widget; // copies of attributes for lambda captures TAttribute PropertyEnabledAttribute = InOwnerTreeNode->IsPropertyEditingEnabled(); TAttribute RowEditConditionAttribute = WidgetRow.EditConditionValue; TAttribute RowIsEnabledAttribute = WidgetRow.IsEnabledAttr; TAttribute IsEnabledAttribute = TAttribute::CreateLambda( [PropertyEnabledAttribute, RowIsEnabledAttribute, RowEditConditionAttribute]() { return PropertyEnabledAttribute.Get(true) && RowIsEnabledAttribute.Get(true) && RowEditConditionAttribute.Get(true); }); TAttribute RowIsValueEnabledAttribute = WidgetRow.IsValueEnabledAttr; TAttribute IsValueEnabledAttribute = TAttribute::CreateLambda( [IsEnabledAttribute, RowIsValueEnabledAttribute]() { return IsEnabledAttribute.Get() && RowIsValueEnabledAttribute.Get(true); }); NameWidget->SetEnabled(IsEnabledAttribute); ValueWidget->SetEnabled(IsValueEnabledAttribute); ExtensionWidget->SetEnabled(IsEnabledAttribute); TSharedRef 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 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 InRow = SharedThis(this); // Let the handler subclasses decide if they want the handle widget. if (WidgetRow.CustomDragDropHandler->UseHandleWidget()) { TSharedRef 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 PropertyNode = Customization->GetPropertyNode()) { if (PropertyNode->IsReorderable()) { TSharedPtr InRow = SharedThis(this); TSharedRef 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(PropertyNode->GetProperty()) != nullptr && CastField(CastField(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 RightmostWidget; if (GetPropertyNode().IsValid() && DetailsView && DetailsView->GetDisplayManager().IsValid()) { TSharedPtr 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> 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 PropertyNode = InOwnerTreeNode->GetPropertyNode(); TStrongObjectPtr PinnedObject; if (!Objects.IsEmpty()) { PinnedObject = Objects[0].Pin(); } if (PropertyNode && PinnedObject.IsValid()) { TSharedRef 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 IsHovered = TAttribute::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 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::Create([this]() { return IsResetToDefaultVisible() ? EnabledResetToDefaultIcon : DisabledResetToDefaultIcon; }); ResetToDefault.ToolTip = TAttribute::Create([this]() { return IsResetToDefaultVisible() ? NSLOCTEXT("PropertyEditor", "ResetToDefaultPropertyValueToolTip", "Reset this property to its default value.") : FText::GetEmpty(); }); CreateGlobalExtensionWidgets(ExtensionButtons); FSlimHorizontalToolBarBuilder ToolbarBuilder(TSharedPtr(), 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 PropertyNode = GetPropertyNode()) { // Mark property node as animating so we will animate after any potential re-construction TSharedPtr 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 PropertyNode = GetPropertyNode(); const TSharedPtr PropertyHandle = GetPropertyHandle(); if (PropertyNode.IsValid() && PropertyHandle.IsValid()) { TSharedPtr DetailsView = OwnerTreeNode.Pin()->GetDetailsViewSharedPtr(); TSharedPtr Handle = PropertyEditorHelpers::GetPropertyHandle(PropertyNode.ToSharedRef(), DetailsView->GetNotifyHook(), DetailsView->GetPropertyUtilities()); FString Value; if (Handle->GetValueAsFormattedString(Value, PPF_Copy) == FPropertyAccess::Success) { FProperty* Property = PropertyHandle->GetProperty(); TSharedRef 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 PropertyHandle = GetPropertyHandle(); if (WidgetRow.CustomResetToDefault.IsSet()) { WidgetRow.CustomResetToDefault.GetValue().OnResetToDefaultClicked(PropertyHandle); } else if (PropertyHandle.IsValid()) { TSharedPtr 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> GroupProperties = GetPropertyHandles(true); !GroupProperties.IsEmpty()) { TArray PropertiesNotCopied; PropertiesNotCopied.Reserve(GroupProperties.Num()); TMap PropertyValues; PropertyValues.Reserve(GroupProperties.Num()); for (TSharedPtr 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& OutTaggedClipboard) { for (const TPair& PropertyValuePair : PropertyValues) { OutTaggedClipboard.Add(FName(PropertyValuePair.Key), PropertyValuePair.Value); } }); PulseAnimation.Play(SharedThis(this)); } } bool SDetailSingleItemRow::CanCopyGroup() const { if (!OwnerTreeNode.IsValid()) { return false; } TArray> GroupPropertyHandles = GetPropertyHandles(true); return !GroupPropertyHandles.IsEmpty(); } void SDetailSingleItemRow::OnPasteGroup() { if (!OwnerTreeNode.IsValid() || !CanPasteGroup()) { return; } if (OnPasteFromTextDelegate.IsValid()) { if (const TArray> GroupProperties = GetPropertyHandles(true); !GroupProperties.IsEmpty()) { FScopedTransaction Transaction(NSLOCTEXT("UnrealEd", "PasteGroupProperties", "Paste Group Properties")); TArray PropertiesNotPasted; PropertiesNotPasted.Reserve(GroupProperties.Num()); { const FGuid OperationGuid = FGuid::NewGuid(); for (const TPair& 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> GroupPropertyHandles = GetPropertyHandles(true); const TArray> 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& 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 PropertyNames; Algo::Transform(GroupPropertyHandles, PropertyNames, [](const TSharedPtr& 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 OwningDetailsView; if (TSharedPtr 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 Label; TAttribute ToolTip; if (bIsGroup) { Label = NSLOCTEXT("PropertyView", "CopyGroupProperties", "Copy All Properties in Group"); ToolTip = TAttribute::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 Label; TAttribute ToolTip; if (bIsGroup) { Label = NSLOCTEXT("PropertyView", "PasteGroupProperties", "Paste All Properties in Group"); ToolTip = TAttribute::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> SDetailSingleItemRow::GetPropertyHandles(const bool bRecursive) const { if (TArray> PropertyHandles = SDetailTableRowBase::GetPropertyHandles(bRecursive); !PropertyHandles.IsEmpty()) { return PropertyHandles; } return WidgetRow.GetPropertyHandles(); } TSharedPtr SDetailSingleItemRow::GetPrimaryPropertyHandle() const { TSharedPtr PrimaryPropertyHandle = SDetailTableRowBase::GetPrimaryPropertyHandle(); if (PrimaryPropertyHandle.IsValid()) { return PrimaryPropertyHandle; } if (const TArray>& WidgetPropertyHandles = WidgetRow.GetPropertyHandles(); !WidgetPropertyHandles.IsEmpty()) { return WidgetPropertyHandles[0]; } return nullptr; } void SDetailSingleItemRow::OnCopyProperty() { if (OwnerTreeNode.IsValid()) { TSharedPtr PropertyNode = GetPropertyNode(); if (PropertyNode.IsValid()) { TSharedPtr DetailsView = OwnerTreeNode.Pin()->GetDetailsViewSharedPtr(); TSharedPtr 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 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 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 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 SDetailSingleItemRow::GetNameWidget(TSharedRef NameWidget, const TSharedPtr& Node) const { if (Node.IsValid() && OwnerTreeNode.IsValid() ) { const TSharedPtr DetailTreeNodeSP = OwnerTreeNode.Pin(); if (DetailTreeNodeSP.IsValid()) { if (TSharedPtr DetailsView = DetailTreeNodeSP->GetDetailsViewSharedPtr()) { const TSharedPtr 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 PropertyNode = GetPropertyNode(); // Prevent paste if we cannot find the property node to paste into. if (!PropertyNode.IsValid()) { return false; } const TSharedPtr 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 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 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 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& InOperationId) { if (PasteFromText(InTag, InText)) { TSharedPtr PropertyNode = GetPropertyNode(); if (PropertyNode.IsValid()) { // Mark property node as animating so we will animate after re-construction TSharedPtr 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 PropertyHandle = GetPropertyHandle(); if (!PropertyHandle.IsValid()) { return false; } if (PropertyHandle->SetValueFromFormattedString(InText, PropertyValueSetFlags) != FPropertyAccess::Success) { return false; } if (bIsTagged) { TArray 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 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 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 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 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 Owner = OwnerTreeNode.Pin()) { return FPropertyEditorPermissionList::Get().IsSpecificPropertyDenyListed(Owner->GetParentBaseStructure(), Owner->GetNodeName()); } return false; } void SDetailSingleItemRow::PopulateExtensionWidget() { TSharedPtr OwnerTreeNodePinned = OwnerTreeNode.Pin(); if (OwnerTreeNodePinned.IsValid()) { TSharedPtr DetailsView = OwnerTreeNodePinned->GetDetailsViewSharedPtr(); TSharedPtr ExtensionHandler = DetailsView->GetExtensionHandler(); if (Customization->HasPropertyNode() && ExtensionHandler.IsValid()) { TSharedPtr 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 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 OwnerTreeNodePinned = OwnerTreeNode.Pin(); if (OwnerTreeNodePinned.IsValid()) { TSharedPtr ParentCategory = OwnerTreeNodePinned->GetParentCategory(); if (ParentCategory.IsValid() && ParentCategory->IsFavoriteCategory()) { return true; } TSharedPtr 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 OwnerTreeNodePinned = OwnerTreeNode.Pin(); if (!OwnerTreeNodePinned.IsValid()) { return; } TSharedPtr DetailsView = OwnerTreeNodePinned->GetDetailsViewSharedPtr(); bool bNewValue = !IsFavorite(); if (Customization->HasPropertyNode()) { TSharedPtr PropertyNode = Customization->GetPropertyNode(); PropertyNode->SetFavorite(bNewValue); } else if (Customization->HasCustomBuilder()) { TSharedPtr PropertyHandle = Customization->CustomBuilderRow->GetPropertyHandle(); const FString& OriginalPath = Customization->CustomBuilderRow->GetOriginalPath(); if (PropertyHandle.IsValid()) { StaticCastSharedPtr(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& OutExtensions) const { // fetch global extension widgets FPropertyEditorModule& PropertyEditorModule = FModuleManager::Get().GetModuleChecked("PropertyEditor"); FOnGenerateGlobalRowExtensionArgs Args; Args.OwnerTreeNode = OwnerTreeNode; if (Customization->HasPropertyNode()) { Args.PropertyHandle = PropertyEditorHelpers::GetPropertyHandle(Customization->GetPropertyNode().ToSharedRef(), nullptr, nullptr); } else if (const TSharedPtr PrimaryPropertyHandle = GetPrimaryPropertyHandle(); PrimaryPropertyHandle.IsValid() && PrimaryPropertyHandle->IsValidHandle()) { Args.PropertyHandle = PrimaryPropertyHandle; } PropertyEditorModule.GetGlobalRowExtensionDelegate().Broadcast(Args, OutExtensions); } bool SDetailSingleItemRow::IsHighlighted() const { TSharedPtr OwnerTreeNodePtr = OwnerTreeNode.Pin(); return OwnerTreeNodePtr.IsValid() ? OwnerTreeNodePtr->IsHighlighted() : false; } TSharedPtr SDetailSingleItemRow::CreateDragDropOperation() { if (WidgetRow.CustomDragDropHandler) { TSharedPtr DragOp = WidgetRow.CustomDragDropHandler->CreateDragDropOperation(); DragOperation = DragOp; return DragOp; } else { TSharedPtr 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 DragDropOp = ParentRow.Pin()->CreateDragDropOperation(); if (DragDropOp.IsValid()) { return FReply::Handled().BeginDragDrop(DragDropOp.ToSharedRef()); } } return FReply::Unhandled(); } FArrayRowDragDropOp::FArrayRowDragDropOp(TSharedPtr 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"); } }