// Copyright Epic Games, Inc. All Rights Reserved. #include "SkeletonTreeVirtualBoneItem.h" #include "Widgets/Input/SSpinBox.h" #include "Widgets/Text/STextBlock.h" #include "SSkeletonTreeRow.h" #include "IPersonaPreviewScene.h" #include "Styling/AppStyle.h" #include "Widgets/Images/SImage.h" #include "Widgets/Text/SInlineEditableTextBlock.h" #include "Widgets/Views/SListView.h" #include "Containers/UnrealString.h" #include "Animation/DebugSkelMeshComponent.h" #include "Animation/BlendProfile.h" #include "UObject/Package.h" #include "SocketDragDropOp.h" #include "Editor.h" #include "ScopedTransaction.h" #include "Preferences/PersonaOptions.h" #define LOCTEXT_NAMESPACE "FSkeletonTreeVirtualBoneItem" FSkeletonTreeVirtualBoneItem::FSkeletonTreeVirtualBoneItem(const FName& InBoneName, const TSharedRef& InSkeletonTree) : FSkeletonTreeItem(InSkeletonTree) , BoneName(InBoneName) { static const FString BoneProxyPrefix(TEXT("VIRTUALBONEPROXY_")); BoneProxy = NewObject(GetTransientPackage(), *(BoneProxyPrefix + FString::Printf(TEXT("%p"), &InSkeletonTree.Get()) + InBoneName.ToString())); BoneProxy->SetFlags(RF_Transactional); BoneProxy->BoneName = InBoneName; BoneProxy->bIsTransformEditable = false; TSharedPtr PreviewScene = InSkeletonTree->GetPreviewScene(); if (PreviewScene.IsValid()) { BoneProxy->SkelMeshComponent = PreviewScene->GetPreviewMeshComponent(); BoneProxy->WeakPreviewScene = PreviewScene.ToWeakPtr(); } } EVisibility FSkeletonTreeVirtualBoneItem::GetLODIconVisibility() const { return EVisibility::Visible; } void FSkeletonTreeVirtualBoneItem::GenerateWidgetForNameColumn(TSharedPtr< SHorizontalBox > Box, const TAttribute& FilterText, FIsSelected InIsSelected) { const FSlateBrush* LODIcon = FAppStyle::Get().GetBrush("SkeletonTree.Bone"); Box->AddSlot() .AutoWidth() .Padding(FMargin(0.0f, 2.0f)) .VAlign(VAlign_Center) .HAlign(HAlign_Center) [ SNew(SImage) .ColorAndOpacity(this, &FSkeletonTreeVirtualBoneItem::GetBoneTextColor, InIsSelected) .Image(LODIcon) .Visibility(this, &FSkeletonTreeVirtualBoneItem::GetLODIconVisibility) ]; FText ToolTip = GetBoneToolTip(); TAttribute NameAttr = TAttribute::Create(TAttribute::FGetter::CreateSP(this, &FSkeletonTreeVirtualBoneItem::GetVirtualBoneNameAsText)); InlineWidget = SNew(SInlineEditableTextBlock) .ColorAndOpacity(this, &FSkeletonTreeVirtualBoneItem::GetBoneTextColor, InIsSelected) .Text(NameAttr) .HighlightText(FilterText) .Font(this, &FSkeletonTreeVirtualBoneItem::GetBoneTextFont) .ToolTipText(ToolTip) .OnEnterEditingMode(this, &FSkeletonTreeVirtualBoneItem::OnVirtualBoneNameEditing) .OnVerifyTextChanged(this, &FSkeletonTreeVirtualBoneItem::OnVerifyBoneNameChanged) .OnTextCommitted(this, &FSkeletonTreeVirtualBoneItem::OnCommitVirtualBoneName) .IsSelected(InIsSelected); OnRenameRequested.BindSP(InlineWidget.Get(), &SInlineEditableTextBlock::EnterEditingMode); Box->AddSlot() .AutoWidth() .Padding(4, 0, 0, 0) [ SNew(STextBlock) .ColorAndOpacity(this, &FSkeletonTreeVirtualBoneItem::GetBoneTextColor, InIsSelected) .Text(FText::FromString(VirtualBoneNameHelpers::VirtualBonePrefix)) .Font(this, &FSkeletonTreeVirtualBoneItem::GetBoneTextFont) .Visibility(this, &FSkeletonTreeVirtualBoneItem::GetVirtualBonePrefixVisibility) ]; Box->AddSlot() .Padding(4, 0, 0, 0) .AutoWidth() [ InlineWidget.ToSharedRef() ]; Box->AddSlot() .AutoWidth() .Padding(4, 0, 0, 0) .VAlign(VAlign_Center) [ SNew (STextBlock) .ColorAndOpacity(this, &FSkeletonTreeVirtualBoneItem::GetBoneTextColor, InIsSelected) .Text_Lambda( [BoneName=BoneName, EditableSkeleton=GetEditableSkeleton()]() -> FText { const int32 BoneIndex = EditableSkeleton->GetSkeleton().GetReferenceSkeleton().FindBoneIndex(BoneName); return FText::Format(LOCTEXT("VirtualBoneIndexValue", "<{0}>"), FText::AsNumber(BoneIndex)); }) .Font(this, &FSkeletonTreeVirtualBoneItem::GetBoneTextFont) .ToolTipText( LOCTEXT("VirtualBoneIndexTooltip", "The index of this virtual bone in the reference skeleton's flat list of bones.") ) .Visibility_Lambda([]() -> EVisibility { return GetDefault()->bShowBoneIndexes ? EVisibility::Visible : EVisibility::Collapsed; }) ]; } TSharedRef< SWidget > FSkeletonTreeVirtualBoneItem::GenerateWidgetForDataColumn(const FName& DataColumnName, FIsSelected InIsSelected) { if (DataColumnName == ISkeletonTree::Columns::BlendProfile) { return SNew(SBox) .Padding(0.0f) .HAlign(HAlign_Fill) .VAlign(VAlign_Center) [ SNew(SSpinBox) .Visibility(this, &FSkeletonTreeVirtualBoneItem::GetBoneBlendProfileVisibility) .Style(&FAppStyle::Get(), "SkeletonTree.HyperlinkSpinBox") .Font(FAppStyle::Get().GetFontStyle("SmallFont")) .ContentPadding(0.0f) .Delta(0.01f) .MinValue(0.0f) .MinSliderValue(this, &FSkeletonTreeVirtualBoneItem::GetBlendProfileMinSliderValue) .MaxSliderValue(this, &FSkeletonTreeVirtualBoneItem::GetBlendProfileMaxSliderValue) .Value(this, &FSkeletonTreeVirtualBoneItem::GetBoneBlendProfileScale) .OnValueCommitted(this, &FSkeletonTreeVirtualBoneItem::OnBlendSliderCommitted) .OnValueChanged(this, &FSkeletonTreeVirtualBoneItem::OnBlendSliderChanged) .OnBeginSliderMovement(this, &FSkeletonTreeVirtualBoneItem::OnBeginBlendSliderMovement) .OnEndSliderMovement(this, &FSkeletonTreeVirtualBoneItem::OnEndBlendSliderMovement) .ClearKeyboardFocusOnCommit(true) ]; } return SNullWidget::NullWidget; } EVisibility FSkeletonTreeVirtualBoneItem::GetBoneBlendProfileVisibility() const { return GetSkeletonTree()->GetSelectedBlendProfile() ? EVisibility::Visible : EVisibility::Collapsed; } float FSkeletonTreeVirtualBoneItem::GetBoneBlendProfileScale() const { if (UBlendProfile* CurrentProfile = GetSkeletonTree()->GetSelectedBlendProfile()) { return CurrentProfile->GetBoneBlendScale(BoneName); } return 0.0; } TOptional FSkeletonTreeVirtualBoneItem::GetBlendProfileMaxSliderValue() const { if (UBlendProfile* CurrentProfile = GetSkeletonTree()->GetSelectedBlendProfile()) { return (CurrentProfile->GetMode() == EBlendProfileMode::WeightFactor) ? 10.0f : 1.0f; } return 1.0f; } TOptional FSkeletonTreeVirtualBoneItem::GetBlendProfileMinSliderValue() const { if (UBlendProfile* CurrentProfile = GetSkeletonTree()->GetSelectedBlendProfile()) { return (CurrentProfile->GetMode() == EBlendProfileMode::WeightFactor) ? 1.0f : 0.0f; } return 0.0f; } void FSkeletonTreeVirtualBoneItem::OnBeginBlendSliderMovement() { if (bBlendSliderStartedTransaction == false) { bBlendSliderStartedTransaction = true; GEditor->BeginTransaction(LOCTEXT("BlendSliderTransation", "Set Blend Profile Value")); const FName& BlendProfileName = GetSkeletonTree()->GetSelectedBlendProfile()->GetFName(); UBlendProfile* BlendProfile = GetEditableSkeleton()->GetBlendProfile(BlendProfileName); if (BlendProfile) { BlendProfile->SetFlags(RF_Transactional); BlendProfile->Modify(); } } } void FSkeletonTreeVirtualBoneItem::OnEndBlendSliderMovement(float NewValue) { if (bBlendSliderStartedTransaction) { GEditor->EndTransaction(); bBlendSliderStartedTransaction = false; } } void FSkeletonTreeVirtualBoneItem::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 FSkeletonTreeVirtualBoneItem::OnBlendSliderChanged(float NewValue) { const FName& BlendProfileName = GetSkeletonTree()->GetSelectedBlendProfile()->GetFName(); UBlendProfile* BlendProfile = GetEditableSkeleton()->GetBlendProfile(BlendProfileName); if (BlendProfile) { BlendProfile->SetBoneBlendScale(BoneName, NewValue, false, true); } } FSlateFontInfo FSkeletonTreeVirtualBoneItem::GetBoneTextFont() const { return FAppStyle::GetWidgetStyle("SkeletonTree.NormalFont").Font; } FSlateColor FSkeletonTreeVirtualBoneItem::GetBoneTextColor(FIsSelected InIsSelected) const { bool bIsSelected = false; if (InIsSelected.IsBound()) { bIsSelected = InIsSelected.Execute(); } if(bIsSelected) { return FSlateColor::UseForeground(); } else { return FSlateColor(FLinearColor(0.4f, 0.4f, 1.f)); } } FText FSkeletonTreeVirtualBoneItem::GetBoneToolTip() { return LOCTEXT("VirtualBone_ToolTip", "Virtual Bones are added in editor and allow space switching between two different bones in the skeleton."); } void FSkeletonTreeVirtualBoneItem::OnItemDoubleClicked() { OnRenameRequested.ExecuteIfBound(); } void FSkeletonTreeVirtualBoneItem::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 FSkeletonTreeVirtualBoneItem::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 FSkeletonTreeVirtualBoneItem::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() ? GetSkeletonTree()->GetPreviewScene()->GetPreviewMeshComponent()->GetSkeletalMeshAsset() : nullptr; GetEditableSkeleton()->SetSocketParent(SocketInfo.Socket->SocketName, BoneName, SkeletalMesh); return FReply::Handled(); } } return FReply::Unhandled(); } void FSkeletonTreeVirtualBoneItem::RequestRename() { OnRenameRequested.ExecuteIfBound(); } void FSkeletonTreeVirtualBoneItem::OnVirtualBoneNameEditing() { CachedBoneNameForRename = BoneName; BoneName = VirtualBoneNameHelpers::RemoveVirtualBonePrefix(BoneName.ToString()); } bool FSkeletonTreeVirtualBoneItem::OnVerifyBoneNameChanged(const FText& InText, FText& OutErrorMessage) { bool bVerifyName = true; FString InTextTrimmed = FText::TrimPrecedingAndTrailing(InText).ToString(); FString NewName = VirtualBoneNameHelpers::AddVirtualBonePrefix(InTextTrimmed); if (InTextTrimmed.IsEmpty()) { OutErrorMessage = LOCTEXT("EmptyVirtualBoneName_Error", "Virtual bones must have a name!"); bVerifyName = false; } else { if(InTextTrimmed != BoneName.ToString()) { bVerifyName = !GetEditableSkeleton()->DoesVirtualBoneAlreadyExist(NewName); // Needs to be checked on verify. if (!bVerifyName) { // Tell the user that the name is a duplicate OutErrorMessage = LOCTEXT("DuplicateVirtualBone_Error", "Name in use!"); bVerifyName = false; } } } return bVerifyName; } void FSkeletonTreeVirtualBoneItem::OnCommitVirtualBoneName(const FText& InText, ETextCommit::Type CommitInfo) { FString NewNameString = VirtualBoneNameHelpers::AddVirtualBonePrefix(FText::TrimPrecedingAndTrailing(InText).ToString()); FName NewName(*NewNameString); // Notify skeleton tree of rename GetEditableSkeleton()->RenameVirtualBone(CachedBoneNameForRename, NewName); BoneName = NewName; } EVisibility FSkeletonTreeVirtualBoneItem::GetVirtualBonePrefixVisibility() const { return InlineWidget->IsInEditMode() ? EVisibility::Visible : EVisibility::Collapsed; } void FSkeletonTreeVirtualBoneItem::EnableBoneProxyTick(bool bEnable) { BoneProxy->bIsTickable = bEnable; } void FSkeletonTreeVirtualBoneItem::AddReferencedObjects( FReferenceCollector& Collector ) { Collector.AddReferencedObject(BoneProxy); } #undef LOCTEXT_NAMESPACE