// Copyright Epic Games, Inc. All Rights Reserved. #include "SkeletonTreeBoneItem.h" #include "SSkeletonTreeRow.h" #include "IPersonaPreviewScene.h" #include "IDocumentation.h" #include "Animation/BlendProfile.h" #include "IEditableSkeleton.h" #include "Styling/AppStyle.h" #include "Widgets/Images/SImage.h" #include "BoneDragDropOp.h" #include "Animation/DebugSkelMeshComponent.h" #include "SocketDragDropOp.h" #include "DragAndDrop/AssetDragDropOp.h" #include "Widgets/Input/SComboButton.h" #include "Widgets/Input/SSpinBox.h" #include "Widgets/SToolTip.h" #include "Widgets/Views/SListView.h" #include "Textures/SlateIcon.h" #include "Framework/Commands/UIAction.h" #include "Framework/MultiBox/MultiBoxBuilder.h" #include "Rendering/SkeletalMeshRenderData.h" #include "UObject/Package.h" #include "Editor.h" #include "ScopedTransaction.h" #include "Preferences/PersonaOptions.h" #define LOCTEXT_NAMESPACE "FSkeletonTreeBoneItem" FSkeletonTreeBoneItem::FSkeletonTreeBoneItem(const FName& InBoneName, const TSharedRef& InSkeletonTree) : FSkeletonTreeItem(InSkeletonTree) , BoneName(InBoneName) , bWeightedBone(false) , bRequiredBone(false) , bBlendSliderStartedTransaction(false) { static const FString BoneProxyPrefix(TEXT("BONEPROXY_")); BoneProxy = NewObject(GetTransientPackage(), *(BoneProxyPrefix + FString::Printf(TEXT("%p"), &InSkeletonTree.Get()) + InBoneName.ToString())); BoneProxy->SetFlags(RF_Transactional); BoneProxy->BoneName = InBoneName; const TSharedPtr PreviewScene = InSkeletonTree->GetPreviewScene(); if (PreviewScene.IsValid()) { BoneProxy->SkelMeshComponent = PreviewScene->GetPreviewMeshComponent(); BoneProxy->WeakPreviewScene = PreviewScene.ToWeakPtr(); } } const FSlateBrush* FSkeletonTreeBoneItem::GetLODIcon() const { if (!bRequiredBone) { return FAppStyle::Get().GetBrush("SkeletonTree.NonRequiredBone"); } else if (!bWeightedBone) { return FAppStyle::Get().GetBrush("SkeletonTree.BoneNonWeighted"); } return FAppStyle::Get().GetBrush("SkeletonTree.Bone"); } void FSkeletonTreeBoneItem::GenerateWidgetForNameColumn( TSharedPtr< SHorizontalBox > Box, const TAttribute& FilterText, FIsSelected InIsSelected ) { const FSlateBrush* LODIcon = FAppStyle::GetBrush("SkeletonTree.Bone"); Box->AddSlot() .AutoWidth() .Padding(FMargin(0.0f, 2.0f)) .VAlign(VAlign_Center) .HAlign(HAlign_Center) [ SNew(SImage) .ColorAndOpacity(this, &FSkeletonTreeBoneItem::GetBoneTextColor, InIsSelected) .Image(this, &FSkeletonTreeBoneItem::GetLODIcon) ]; if (GetSkeletonTree()->GetPreviewScene().IsValid()) { UDebugSkelMeshComponent* PreviewComponent = GetSkeletonTree()->GetPreviewScene()->GetPreviewMeshComponent(); CacheLODChange(PreviewComponent); } FText ToolTip = GetBoneToolTip(); Box->AddSlot() .AutoWidth() .Padding(4, 0, 0, 0) .VAlign(VAlign_Center) [ SNew( STextBlock ) .ColorAndOpacity(this, &FSkeletonTreeBoneItem::GetBoneTextColor, InIsSelected) .Text( FText::FromName(BoneName) ) .HighlightText( FilterText ) .Font(this, &FSkeletonTreeBoneItem::GetBoneTextFont) .ToolTipText( ToolTip ) ]; Box->AddSlot() .AutoWidth() .Padding(4, 0, 0, 0) .VAlign(VAlign_Center) [ SNew (STextBlock) .ColorAndOpacity(this, &FSkeletonTreeBoneItem::GetBoneTextColor, InIsSelected) .Text_Lambda( [BoneName=BoneName, EditableSkeleton=GetEditableSkeleton()]() -> FText { const int32 BoneIndex = EditableSkeleton->GetSkeleton().GetReferenceSkeleton().FindBoneIndex(BoneName); return FText::Format(LOCTEXT("BoneIndexValue", "<{0}>"), FText::AsNumber(BoneIndex)); }) .Font(this, &FSkeletonTreeBoneItem::GetBoneTextFont) .ToolTipText( LOCTEXT("BoneIndexTooltip", "The index of this bone in the reference skeleton's flat list of bones.") ) .Visibility_Lambda([]() -> EVisibility { return GetDefault()->bShowBoneIndexes ? EVisibility::Visible : EVisibility::Collapsed; }) ]; } TSharedRef< SWidget > FSkeletonTreeBoneItem::GenerateWidgetForDataColumn(const FName& DataColumnName, FIsSelected InIsSelected) { if(DataColumnName == ISkeletonTree::Columns::Retargeting) { return SNew(SHorizontalBox) +SHorizontalBox::Slot() .HAlign(HAlign_Left) .Padding(0.0f) [ SAssignNew(RetargetingComboButton, SComboButton) .ComboButtonStyle( &FAppStyle::Get().GetWidgetStyle< FComboButtonStyle >("SkeletonTree.RetargetingComboButton")) .ForegroundColor(this, &FSkeletonTreeBoneItem::GetBoneTextColor, InIsSelected) .ContentPadding(0.f) .OnGetMenuContent(this, &FSkeletonTreeBoneItem::CreateBoneTranslationRetargetingModeMenu) .ToolTip(IDocumentation::Get()->CreateToolTip( LOCTEXT("RetargetingToolTip", "Set bone translation retargeting mode"), nullptr, TEXT("Shared/Editors/Persona"), TEXT("TranslationRetargeting"))) .VAlign(VAlign_Center) .ButtonContent() [ SNew(STextBlock) .Text(this, &FSkeletonTreeBoneItem::GetTranslationRetargetingModeMenuTitle) ] ]; } else if(DataColumnName == ISkeletonTree::Columns::BlendProfile) { return SNew(SBox) .Padding(0.0f) .HAlign(HAlign_Fill) .VAlign(VAlign_Center) [ SNew(SSpinBox) .Visibility(this, &FSkeletonTreeBoneItem::GetBoneBlendProfileVisibility) .Style(&FAppStyle::Get(), "SkeletonTree.HyperlinkSpinBox") .Font(FAppStyle::Get().GetFontStyle("SmallFont")) .ContentPadding(0.0f) .Delta(0.01f) .MinValue(0.0f) .MinSliderValue(this, &FSkeletonTreeBoneItem::GetBlendProfileMinSliderValue) .MaxSliderValue(this, &FSkeletonTreeBoneItem::GetBlendProfileMaxSliderValue) .Value(this, &FSkeletonTreeBoneItem::GetBoneBlendProfileScale) .OnValueCommitted(this, &FSkeletonTreeBoneItem::OnBlendSliderCommitted) .OnValueChanged(this, &FSkeletonTreeBoneItem::OnBlendSliderChanged) .OnBeginSliderMovement(this, &FSkeletonTreeBoneItem::OnBeginBlendSliderMovement) .OnEndSliderMovement(this, &FSkeletonTreeBoneItem::OnEndBlendSliderMovement) .ClearKeyboardFocusOnCommit(true) ]; } return SNullWidget::NullWidget; } EVisibility FSkeletonTreeBoneItem::GetBoneBlendProfileVisibility() const { return GetSkeletonTree()->GetSelectedBlendProfile() ? EVisibility::Visible : EVisibility::Collapsed; } float FSkeletonTreeBoneItem::GetBoneBlendProfileScale() const { if (UBlendProfile* CurrentProfile = GetSkeletonTree()->GetSelectedBlendProfile()) { return CurrentProfile->GetBoneBlendScale(BoneName); } return 0.0; } TOptional FSkeletonTreeBoneItem::GetBlendProfileMaxSliderValue() const { if (UBlendProfile* CurrentProfile = GetSkeletonTree()->GetSelectedBlendProfile()) { return (CurrentProfile->GetMode() == EBlendProfileMode::WeightFactor) ? 10.0f : 1.0f; } return 1.0f; } TOptional FSkeletonTreeBoneItem::GetBlendProfileMinSliderValue() const { if (UBlendProfile* CurrentProfile = GetSkeletonTree()->GetSelectedBlendProfile()) { return (CurrentProfile->GetMode() == EBlendProfileMode::WeightFactor) ? 1.0f : 0.0f; } return 0.0f; } FSlateColor FSkeletonTreeBoneItem::GetRetargetingComboButtonForegroundColor() const { static const FName InvertedForegroundName("InvertedForeground"); static const FName DefaultForegroundName("DefaultForeground"); if (RetargetingComboButton.IsValid()) { return RetargetingComboButton->IsHovered() ? FAppStyle::GetSlateColor(InvertedForegroundName) : FAppStyle::GetSlateColor(DefaultForegroundName); } return FSlateColor::UseForeground(); } TSharedRef< SWidget > FSkeletonTreeBoneItem::CreateBoneTranslationRetargetingModeMenu() { FMenuBuilder MenuBuilder(true, nullptr); MenuBuilder.BeginSection("BoneTranslationRetargetingMode", LOCTEXT( "BoneTranslationRetargetingModeMenuHeading", "Bone Translation Retargeting Mode" ) ); { UEnum* const Enum = StaticEnum(); check(Enum); FUIAction ActionRetargetingAnimation = FUIAction(FExecuteAction::CreateSP(this, &FSkeletonTreeBoneItem::SetBoneTranslationRetargetingMode, EBoneTranslationRetargetingMode::Animation)); MenuBuilder.AddMenuEntry( Enum->GetDisplayNameTextByValue(EBoneTranslationRetargetingMode::Animation), LOCTEXT( "BoneTranslationRetargetingAnimationToolTip", "Use translation from animation." ), FSlateIcon(), ActionRetargetingAnimation); FUIAction ActionRetargetingSkeleton = FUIAction(FExecuteAction::CreateSP(this, &FSkeletonTreeBoneItem::SetBoneTranslationRetargetingMode, EBoneTranslationRetargetingMode::Skeleton)); MenuBuilder.AddMenuEntry( Enum->GetDisplayNameTextByValue(EBoneTranslationRetargetingMode::Skeleton), LOCTEXT( "BoneTranslationRetargetingSkeletonToolTip", "Use translation from Skeleton." ), FSlateIcon(), ActionRetargetingSkeleton); FUIAction ActionRetargetingLengthScale = FUIAction(FExecuteAction::CreateSP(this, &FSkeletonTreeBoneItem::SetBoneTranslationRetargetingMode, EBoneTranslationRetargetingMode::AnimationScaled)); MenuBuilder.AddMenuEntry( Enum->GetDisplayNameTextByValue(EBoneTranslationRetargetingMode::AnimationScaled), LOCTEXT( "BoneTranslationRetargetingAnimationScaledToolTip", "Use translation from animation, scale length by Skeleton's proportions." ), FSlateIcon(), ActionRetargetingLengthScale); FUIAction ActionRetargetingAnimationRelative = FUIAction(FExecuteAction::CreateSP(this, &FSkeletonTreeBoneItem::SetBoneTranslationRetargetingMode, EBoneTranslationRetargetingMode::AnimationRelative)); MenuBuilder.AddMenuEntry( Enum->GetDisplayNameTextByValue(EBoneTranslationRetargetingMode::AnimationRelative), LOCTEXT("BoneTranslationRetargetingAnimationRelativeToolTip", "Use relative translation from animation similar to an additive animation."), FSlateIcon(), ActionRetargetingAnimationRelative); FUIAction ActionRetargetingOrientAndScale = FUIAction(FExecuteAction::CreateSP(this, &FSkeletonTreeBoneItem::SetBoneTranslationRetargetingMode, EBoneTranslationRetargetingMode::OrientAndScale)); MenuBuilder.AddMenuEntry(Enum->GetDisplayNameTextByValue(EBoneTranslationRetargetingMode::OrientAndScale), LOCTEXT("BoneTranslationRetargetingOrientAndScaleToolTip", "Orient And Scale Translation."), FSlateIcon(), ActionRetargetingOrientAndScale); } MenuBuilder.EndSection(); return MenuBuilder.MakeWidget(); } FText FSkeletonTreeBoneItem::GetTranslationRetargetingModeMenuTitle() const { const USkeleton& Skeleton = GetEditableSkeleton()->GetSkeleton(); const int32 BoneIndex = Skeleton.GetReferenceSkeleton().FindBoneIndex( BoneName ); if( BoneIndex != INDEX_NONE ) { const EBoneTranslationRetargetingMode::Type RetargetingMode = Skeleton.GetBoneTranslationRetargetingMode(BoneIndex); UEnum* const Enum = StaticEnum(); if (Enum) { return Enum->GetDisplayNameTextByValue(RetargetingMode); } } return LOCTEXT("None", "None"); } void FSkeletonTreeBoneItem::SetBoneTranslationRetargetingMode(EBoneTranslationRetargetingMode::Type NewRetargetingMode) { GetEditableSkeleton()->SetBoneTranslationRetargetingMode(BoneName, NewRetargetingMode); } FSlateFontInfo FSkeletonTreeBoneItem::GetBoneTextFont() const { if (!bRequiredBone) { return FAppStyle::GetWidgetStyle("SkeletonTree.ItalicFont").Font; } else { return FAppStyle::GetWidgetStyle("SkeletonTree.NormalFont").Font; } } void FSkeletonTreeBoneItem::CacheLODChange(UDebugSkelMeshComponent* PreviewComponent) { bWeightedBone = false; bRequiredBone = false; if (PreviewComponent) { int32 BoneIndex = PreviewComponent->GetBoneIndex(BoneName); if (BoneIndex != INDEX_NONE) { if (IsBoneWeighted(BoneIndex, PreviewComponent)) { //Bone is vertex weighted bWeightedBone = true; } if (IsBoneRequired(BoneIndex, PreviewComponent)) { bRequiredBone = true; } } } } void FSkeletonTreeBoneItem::EnableBoneProxyTick(bool bEnable) { BoneProxy->bIsTickable = bEnable; } FSlateColor FSkeletonTreeBoneItem::GetBoneTextColor(FIsSelected InIsSelected) const { if (FilterResult == ESkeletonTreeFilterResult::ShownDescendant) { return FSlateColor(FLinearColor::Gray * 0.5f); } bool bIsSelected = InIsSelected.IsBound() ? InIsSelected.Execute() : false; if (bIsSelected) { return FAppStyle::Get().GetSlateColor("Colors.ForegroundInverted"); } else if (bRequiredBone && bWeightedBone) { return FSlateColor::UseForeground(); } else { return FSlateColor::UseSubduedForeground(); } } FReply FSkeletonTreeBoneItem::OnDragDetected(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) { if ( MouseEvent.IsMouseButtonDown( EKeys::LeftMouseButton ) ) { return FReply::Handled().BeginDragDrop( FBoneDragDropOp::New(GetEditableSkeleton(), BoneName ) ); } return FReply::Unhandled(); } FText FSkeletonTreeBoneItem::GetBoneToolTip() { bool bIsMeshBone = false; bool bIsWeightedBone = false; bool bMeshExists = false; FText ToolTip; if (GetSkeletonTree()->GetPreviewScene().IsValid()) { UDebugSkelMeshComponent* PreviewComponent = GetSkeletonTree()->GetPreviewScene()->GetPreviewMeshComponent(); if (PreviewComponent && PreviewComponent->GetSkeletalMeshAsset()) { bMeshExists = true; int32 BoneIndex = PreviewComponent->GetBoneIndex(BoneName); if (BoneIndex != INDEX_NONE) { bIsMeshBone = true; bIsWeightedBone = IsBoneWeighted(BoneIndex, PreviewComponent); } } } if ( !bMeshExists ) { ToolTip = LOCTEXT( "BoneToolTipNoMeshAvailable", "This bone exists only on the skeleton as there is no current mesh set" ); } else { if ( !bIsMeshBone ) { ToolTip = LOCTEXT( "BoneToolTipSkeletonOnly", "This bone exists only on the skeleton, but not on the current mesh" ); } else { if ( !bIsWeightedBone ) { ToolTip = LOCTEXT( "BoneToolTipSkeletonAndMesh", "This bone is used by the current mesh, but has no vertices weighted against it" ); } else { ToolTip = LOCTEXT( "BoneToolTipWeighted", "This bone (or one of its children) has vertices weighted against it" ); } } } return ToolTip; } void FSkeletonTreeBoneItem::OnBeginBlendSliderMovement() { if (bBlendSliderStartedTransaction == false) { bBlendSliderStartedTransaction = true; GEditor->BeginTransaction(LOCTEXT("BlendSliderTransation", "Modify Blend Profile Value")); const FName& BlendProfileName = GetSkeletonTree()->GetSelectedBlendProfile()->GetFName(); UBlendProfile* BlendProfile = GetEditableSkeleton()->GetBlendProfile(BlendProfileName); if (BlendProfile) { BlendProfile->SetFlags(RF_Transactional); BlendProfile->Modify(); } } } void FSkeletonTreeBoneItem::OnEndBlendSliderMovement(float NewValue) { if (bBlendSliderStartedTransaction) { GEditor->EndTransaction(); bBlendSliderStartedTransaction = false; } } void FSkeletonTreeBoneItem::OnBlendSliderCommitted(float NewValue, ETextCommit::Type CommitType) { FName BlendProfileName = GetSkeletonTree()->GetSelectedBlendProfile()->GetFName(); UBlendProfile* BlendProfile = GetEditableSkeleton()->GetBlendProfile(BlendProfileName); if (BlendProfile) { FScopedTransaction Transaction(LOCTEXT("SetBlendProfileValue", "Set Blend Profile Value")); BlendProfile->SetFlags(RF_Transactional); BlendProfile->Modify(); BlendProfile->SetBoneBlendScale(BoneName, NewValue, false, true); } } void FSkeletonTreeBoneItem::OnBlendSliderChanged(float NewValue) { const FName& BlendProfileName = GetSkeletonTree()->GetSelectedBlendProfile()->GetFName(); UBlendProfile* BlendProfile = GetEditableSkeleton()->GetBlendProfile(BlendProfileName); if (BlendProfile) { BlendProfile->SetBoneBlendScale(BoneName, NewValue, false, true); } } void FSkeletonTreeBoneItem::HandleDragEnter(const FDragDropEvent& DragDropEvent) { TSharedPtr DragConnectionOp = DragDropEvent.GetOperationAs(); // Is someone dragging a socket onto a bone? if (DragConnectionOp.IsValid()) { if (BoneName != DragConnectionOp->GetSocketInfo().Socket->BoneName) { // The socket can be dropped here if we're a bone and NOT the socket's existing parent DragConnectionOp->SetIcon( FAppStyle::GetBrush( TEXT( "Graph.ConnectorFeedback.Ok" ) ) ); } else if (DragConnectionOp->IsAltDrag()) { // For Alt-Drag, dropping onto the existing parent is fine, as we're going to copy, not move the socket DragConnectionOp->SetIcon( FAppStyle::GetBrush( TEXT( "Graph.ConnectorFeedback.Ok" ) ) ); } } } void FSkeletonTreeBoneItem::HandleDragLeave(const FDragDropEvent& DragDropEvent) { TSharedPtr DragConnectionOp = DragDropEvent.GetOperationAs(); if (DragConnectionOp.IsValid()) { // Reset the drag/drop icon when leaving this row DragConnectionOp->SetIcon( FAppStyle::GetBrush( TEXT( "Graph.ConnectorFeedback.Error" ) ) ); } } FReply FSkeletonTreeBoneItem::HandleDrop(const FDragDropEvent& DragDropEvent) { TSharedPtr DragConnectionOp = DragDropEvent.GetOperationAs(); if (DragConnectionOp.IsValid()) { FSelectedSocketInfo SocketInfo = DragConnectionOp->GetSocketInfo(); if (DragConnectionOp->IsAltDrag()) { // In an alt-drag, the socket can be dropped on any bone // (including its existing parent) to create a uniquely named copy GetSkeletonTree()->DuplicateAndSelectSocket( SocketInfo, BoneName); } else if (BoneName != SocketInfo.Socket->BoneName) { // The socket can be dropped here if we're a bone and NOT the socket's existing parent USkeletalMesh* SkeletalMesh = GetSkeletonTree()->GetPreviewScene().IsValid() ? ToRawPtr(GetSkeletonTree()->GetPreviewScene()->GetPreviewMeshComponent()->GetSkeletalMeshAsset()) : nullptr; const bool bKeepAbsoluteLocation = DragConnectionOp->IsShiftDrag(); GetEditableSkeleton()->SetSocketParent(SocketInfo.Socket->SocketName, BoneName, SkeletalMesh, bKeepAbsoluteLocation); return FReply::Handled(); } } else { TSharedPtr DragDropOp = DragDropEvent.GetOperationAs(); if (DragDropOp.IsValid()) { //Do we have some assets to attach? if (DragDropOp->HasAssets()) { GetSkeletonTree()->AttachAssets(SharedThis(this), DragDropOp->GetAssets()); } return FReply::Handled(); } } return FReply::Unhandled(); } bool FSkeletonTreeBoneItem::IsBoneWeighted(int32 MeshBoneIndex, UDebugSkelMeshComponent* PreviewComponent) { // MeshBoneIndex must be an index into the mesh's skeleton, *not* the source skeleton!!! if (MeshBoneIndex == INDEX_NONE) { // If we get an invalid index, we are done here return false; } if (!PreviewComponent || !PreviewComponent->GetSkeletalMeshAsset() || !PreviewComponent->GetSkeletalMeshAsset()->GetResourceForRendering() || !PreviewComponent->GetSkeletalMeshAsset()->GetResourceForRendering()->LODRenderData.Num()) { // If there's no mesh, then this bone can't possibly be weighted! return false; } //Get current LOD const int32 LODIndex = FMath::Clamp(PreviewComponent->GetPredictedLODLevel(), 0, PreviewComponent->GetSkeletalMeshAsset()->GetResourceForRendering()->LODRenderData.Num() - 1); FSkeletalMeshLODRenderData& LODData = PreviewComponent->GetSkeletalMeshAsset()->GetResourceForRendering()->LODRenderData[LODIndex]; //Check whether the bone is vertex weighted int32 Index = LODData.ActiveBoneIndices.Find(IntCastChecked(MeshBoneIndex)); return Index != INDEX_NONE; } bool FSkeletonTreeBoneItem::IsBoneRequired(int32 MeshBoneIndex, UDebugSkelMeshComponent* PreviewComponent) { // MeshBoneIndex must be an index into the mesh's skeleton, *not* the source skeleton!!! if (!PreviewComponent || !PreviewComponent->GetSkeletalMeshAsset() || !PreviewComponent->GetSkeletalMeshAsset()->GetResourceForRendering() || !PreviewComponent->GetSkeletalMeshAsset()->GetResourceForRendering()->LODRenderData.Num()) { // If there's no mesh, then this bone can't possibly be weighted! return false; } //Check whether the bone is vertex weighted or required by physics or other forced requirements (see USkeletalMeshComponent::ComputeRequiredBones) const int32 Index = PreviewComponent->RequiredBones.Find(IntCastChecked(MeshBoneIndex)); return Index != INDEX_NONE; } void FSkeletonTreeBoneItem::AddReferencedObjects( FReferenceCollector& Collector ) { Collector.AddReferencedObject(BoneProxy); } #undef LOCTEXT_NAMESPACE