2626 lines
89 KiB
C++
2626 lines
89 KiB
C++
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
|
|
#include "SSkeletonTree.h"
|
|
#include "Misc/MessageDialog.h"
|
|
#include "Modules/ModuleManager.h"
|
|
#include "UObject/PropertyPortFlags.h"
|
|
#include "UObject/UObjectGlobals.h"
|
|
#include "UObject/PackageReload.h"
|
|
#include "Framework/Application/SlateApplication.h"
|
|
#include "Textures/SlateIcon.h"
|
|
#include "Framework/Commands/UIAction.h"
|
|
#include "UICommandList_Pinnable.h"
|
|
#include "Widgets/Images/SImage.h"
|
|
#include "Framework/MultiBox/MultiBoxBuilder.h"
|
|
#include "Widgets/Layout/SScrollBorder.h"
|
|
#include "Widgets/Input/SComboButton.h"
|
|
#include "Widgets/Input/SSpinBox.h"
|
|
#include "Styling/AppStyle.h"
|
|
#include "ActorFactories/ActorFactory.h"
|
|
#include "Exporters/Exporter.h"
|
|
#include "Sound/SoundBase.h"
|
|
#include "Editor.h"
|
|
#include "Framework/Notifications/NotificationManager.h"
|
|
#include "Widgets/Notifications/SNotificationList.h"
|
|
#include "PropertyEditorModule.h"
|
|
#include "IDetailsView.h"
|
|
|
|
#include "ScopedTransaction.h"
|
|
#include "BoneDragDropOp.h"
|
|
#include "SocketDragDropOp.h"
|
|
#include "SkeletonTreeCommands.h"
|
|
#include "Styling/SlateIconFinder.h"
|
|
#include "DragAndDrop/AssetDragDropOp.h"
|
|
|
|
#include "AssetSelection.h"
|
|
|
|
#include "IContentBrowserSingleton.h"
|
|
#include "ContentBrowserModule.h"
|
|
#include "ComponentAssetBroker.h"
|
|
|
|
#include "AnimPreviewInstance.h"
|
|
|
|
#include "MeshUtilities.h"
|
|
#include "UnrealExporter.h"
|
|
#include "Widgets/Input/SSearchBox.h"
|
|
#include "Widgets/Text/SInlineEditableTextBlock.h"
|
|
#include "Framework/Commands/GenericCommands.h"
|
|
#include "Animation/BlendProfile.h"
|
|
#include "SBlendProfilePicker.h"
|
|
#include "IPersonaPreviewScene.h"
|
|
#include "IDocumentation.h"
|
|
#include "PersonaUtils.h"
|
|
#include "Misc/TextFilterExpressionEvaluator.h"
|
|
#include "SkeletonTreeBoneItem.h"
|
|
#include "SkeletonTreeSocketItem.h"
|
|
#include "SkeletonTreeAttachedAssetItem.h"
|
|
#include "SkeletonTreeVirtualBoneItem.h"
|
|
|
|
#include "BoneSelectionWidget.h"
|
|
#include "SkeletonTreeSelection.h"
|
|
#include "Widgets/Layout/SGridPanel.h"
|
|
|
|
#include "HAL/PlatformApplicationMisc.h"
|
|
#include "Widgets/Views/STreeView.h"
|
|
#include "IPinnedCommandList.h"
|
|
#include "PersonaModule.h"
|
|
#include "SPositiveActionButton.h"
|
|
#include "ToolMenus.h"
|
|
#include "ToolMenuMisc.h"
|
|
#include "SkeletonTreeMenuContext.h"
|
|
|
|
#define LOCTEXT_NAMESPACE "SSkeletonTree"
|
|
|
|
const FName ISkeletonTree::Columns::Name("Name");
|
|
const FName ISkeletonTree::Columns::Retargeting("Retargeting");
|
|
const FName ISkeletonTree::Columns::BlendProfile("BlendProfile");
|
|
const FName ISkeletonTree::Columns::DebugVisualization("DebugVisualization");
|
|
|
|
// This is mostly duplicated from SListView, to allow for us to avoid selecting collapsed items
|
|
template <typename ItemType>
|
|
class SSkeletonTreeView : public STreeView<ItemType>
|
|
{
|
|
public:
|
|
bool Private_CanItemBeSelected(ItemType InItem) const
|
|
{
|
|
return !(InItem->GetFilterResult() == ESkeletonTreeFilterResult::ShownDescendant && GetMutableDefault<UPersonaOptions>()->bHideParentsWhenFiltering);
|
|
}
|
|
|
|
virtual void Private_SelectRangeFromCurrentTo( ItemType InRangeSelectionEnd ) override
|
|
{
|
|
if ( this->SelectionMode.Get() == ESelectionMode::None )
|
|
{
|
|
return;
|
|
}
|
|
|
|
const TArrayView<const ItemType> ItemsSourceRef = this->SListView<ItemType>::GetItems();
|
|
|
|
int32 RangeStartIndex = 0;
|
|
if( TListTypeTraits<ItemType>::IsPtrValid(this->RangeSelectionStart) )
|
|
{
|
|
RangeStartIndex = ItemsSourceRef.Find( TListTypeTraits<ItemType>::NullableItemTypeConvertToItemType( this->RangeSelectionStart ) );
|
|
}
|
|
|
|
int32 RangeEndIndex = ItemsSourceRef.Find( InRangeSelectionEnd );
|
|
|
|
RangeStartIndex = FMath::Clamp(RangeStartIndex, 0, ItemsSourceRef.Num());
|
|
RangeEndIndex = FMath::Clamp(RangeEndIndex, 0, ItemsSourceRef.Num());
|
|
|
|
if (RangeEndIndex < RangeStartIndex)
|
|
{
|
|
Swap( RangeStartIndex, RangeEndIndex );
|
|
}
|
|
|
|
for( int32 ItemIndex = RangeStartIndex; ItemIndex <= RangeEndIndex; ++ItemIndex )
|
|
{
|
|
// check if this item can actually be selected
|
|
if(Private_CanItemBeSelected(ItemsSourceRef[ItemIndex]))
|
|
{
|
|
this->SelectedItems.Add( ItemsSourceRef[ItemIndex] );
|
|
}
|
|
}
|
|
|
|
this->InertialScrollManager.ClearScrollVelocity();
|
|
}
|
|
|
|
virtual void Private_SignalSelectionChanged(ESelectInfo::Type SelectInfo) override
|
|
{
|
|
STreeView<ItemType>::Private_SignalSelectionChanged(SelectInfo);
|
|
|
|
// the SListView does not know about bHideParentsWhenFiltering and will select the boens regardless of their visible
|
|
// ( For example when using select all )
|
|
// this filter out those ones to only keep the ones that can be selected
|
|
{
|
|
TArray<ItemType> FilteredSelection;
|
|
for (const ItemType& Item: this->SelectedItems)
|
|
{
|
|
if (Private_CanItemBeSelected(Item))
|
|
{
|
|
FilteredSelection.Add(Item);
|
|
}
|
|
}
|
|
if (FilteredSelection.Num() != this->SelectedItems.Num())
|
|
{
|
|
this->ClearSelection();
|
|
this->SetItemSelection(FilteredSelection, true, SelectInfo);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
void SSkeletonTree::Construct(const FArguments& InArgs, const TSharedRef<FEditableSkeleton>& InEditableSkeleton, const FSkeletonTreeArgs& InSkeletonTreeArgs)
|
|
{
|
|
if (InSkeletonTreeArgs.bHideBonesByDefault)
|
|
{
|
|
BoneFilter = EBoneFilter::None;
|
|
}
|
|
else
|
|
{
|
|
BoneFilter = EBoneFilter::All;
|
|
}
|
|
SocketFilter = ESocketFilter::Active;
|
|
bSelecting = false;
|
|
|
|
EditableSkeleton = InEditableSkeleton;
|
|
PreviewScene = InSkeletonTreeArgs.PreviewScene;
|
|
IsEditable = InArgs._IsEditable;
|
|
Mode = InSkeletonTreeArgs.Mode;
|
|
bAllowMeshOperations = InSkeletonTreeArgs.bAllowMeshOperations;
|
|
bAllowSkeletonOperations = InSkeletonTreeArgs.bAllowSkeletonOperations;
|
|
bShowDebugVisualizationOptions = InSkeletonTreeArgs.bShowDebugVisualizationOptions;
|
|
Extenders = InSkeletonTreeArgs.Extenders;
|
|
OnGetFilterText = InSkeletonTreeArgs.OnGetFilterText;
|
|
Builder = InSkeletonTreeArgs.Builder;
|
|
if (!Builder.IsValid())
|
|
{
|
|
Builder = MakeShareable(new FSkeletonTreeBuilder(FSkeletonTreeBuilderArgs()));
|
|
}
|
|
|
|
Builder->Initialize(SharedThis(this), InSkeletonTreeArgs.PreviewScene, FOnFilterSkeletonTreeItem::CreateSP(this, &SSkeletonTree::HandleFilterSkeletonTreeItem));
|
|
|
|
ContextName = InSkeletonTreeArgs.ContextName;
|
|
|
|
TextFilterPtr = MakeShareable(new FTextFilterExpressionEvaluator(ETextFilterExpressionEvaluatorMode::BasicString));
|
|
|
|
SetPreviewComponentSocketFilter();
|
|
|
|
// Register delegates
|
|
|
|
if(PreviewScene.IsValid())
|
|
{
|
|
if (TSharedPtr<IPersonaPreviewScene> PreviewScenePtr = PreviewScene.Pin())
|
|
{
|
|
PreviewScenePtr->RegisterOnLODChanged(FSimpleDelegate::CreateSP(this, &SSkeletonTree::OnLODSwitched));
|
|
PreviewScenePtr->RegisterOnPreviewMeshChanged(FOnPreviewMeshChanged::CreateSP(this, &SSkeletonTree::OnPreviewMeshChanged));
|
|
PreviewScenePtr->RegisterOnSelectedBonesChanged(FOnSelectedBonesChanged::CreateSP(this, &SSkeletonTree::HandleSelectedBonesChanged));
|
|
PreviewScenePtr->RegisterOnSelectedSocketChanged(FOnSelectedSocketChanged::CreateSP(this, &SSkeletonTree::HandleSelectedSocketChanged));
|
|
PreviewScenePtr->RegisterOnDeselectAll(FSimpleDelegate::CreateSP(this, &SSkeletonTree::HandleDeselectAll));
|
|
|
|
RegisterOnSelectionChanged(FOnSkeletonTreeSelectionChanged::CreateRaw(PreviewScenePtr.Get(), &IPersonaPreviewScene::HandleSkeletonTreeSelectionChanged));
|
|
}
|
|
}
|
|
|
|
if (InSkeletonTreeArgs.OnSelectionChanged.IsBound())
|
|
{
|
|
RegisterOnSelectionChanged(InSkeletonTreeArgs.OnSelectionChanged);
|
|
}
|
|
|
|
FCoreUObjectDelegates::OnPackageReloaded.AddSP(this, &SSkeletonTree::HandlePackageReloaded);
|
|
|
|
// Create our pinned commands before we bind commands
|
|
IPinnedCommandListModule& PinnedCommandListModule = FModuleManager::LoadModuleChecked<IPinnedCommandListModule>(TEXT("PinnedCommandList"));
|
|
PinnedCommands = PinnedCommandListModule.CreatePinnedCommandList(ContextName);
|
|
|
|
// Register and bind all our menu commands
|
|
FSkeletonTreeCommands::Register();
|
|
BindCommands();
|
|
|
|
RegisterBlendProfileMenu();
|
|
RegisterNewMenu();
|
|
RegisterFilterMenu();
|
|
|
|
this->ChildSlot
|
|
[
|
|
SNew( SOverlay )
|
|
+SOverlay::Slot()
|
|
[
|
|
// Add a border if we are being used as a picker
|
|
SNew(SBorder)
|
|
.Visibility_Lambda([this](){ return Mode == ESkeletonTreeMode::Picker ? EVisibility::Visible: EVisibility::Collapsed; })
|
|
.BorderImage(FAppStyle::Get().GetBrush("Menu.Background"))
|
|
]
|
|
+SOverlay::Slot()
|
|
[
|
|
SNew( SVerticalBox )
|
|
+ SVerticalBox::Slot()
|
|
.AutoHeight()
|
|
.Padding(FMargin(0.f, 2.f))
|
|
[
|
|
SNew(SHorizontalBox)
|
|
+SHorizontalBox::Slot()
|
|
.AutoWidth()
|
|
.VAlign(VAlign_Center)
|
|
.Padding(FMargin(6.f, 0.0))
|
|
[
|
|
SNew(SPositiveActionButton)
|
|
.OnGetMenuContent( this, &SSkeletonTree::CreateNewMenuWidget )
|
|
.Icon(FAppStyle::Get().GetBrush("Icons.Plus"))
|
|
]
|
|
|
|
+SHorizontalBox::Slot()
|
|
.FillWidth(1.0f)
|
|
[
|
|
SAssignNew( NameFilterBox, SSearchBox )
|
|
.SelectAllTextWhenFocused( true )
|
|
.OnTextChanged( this, &SSkeletonTree::OnFilterTextChanged )
|
|
.HintText( LOCTEXT( "SearchBoxHint", "Search Skeleton Tree...") )
|
|
.AddMetaData<FTagMetaData>(TEXT("SkelTree.Search"))
|
|
]
|
|
|
|
+SHorizontalBox::Slot()
|
|
.AutoWidth()
|
|
.Padding(FMargin(6.f, 0.0))
|
|
.VAlign(VAlign_Center)
|
|
[
|
|
SAssignNew(FilterComboButton, SComboButton)
|
|
.Visibility(InSkeletonTreeArgs.bShowFilterMenu ? EVisibility::Visible : EVisibility::Collapsed)
|
|
.ComboButtonStyle(&FAppStyle::Get().GetWidgetStyle<FComboButtonStyle>("SimpleComboButton"))
|
|
.ForegroundColor(FSlateColor::UseStyle())
|
|
.ContentPadding(2.0f)
|
|
.OnGetMenuContent( this, &SSkeletonTree::CreateFilterMenuWidget )
|
|
.ToolTipText( this, &SSkeletonTree::GetFilterMenuTooltip )
|
|
.AddMetaData<FTagMetaData>(TEXT("SkelTree.Bones"))
|
|
.HasDownArrow(true)
|
|
.ButtonContent()
|
|
[
|
|
SNew(SImage)
|
|
.Image(FAppStyle::Get().GetBrush("Icons.Settings"))
|
|
.ColorAndOpacity(FSlateColor::UseForeground())
|
|
]
|
|
]
|
|
]
|
|
|
|
+ SVerticalBox::Slot()
|
|
.Padding( FMargin( 0.0f, 2.0f, 0.0f, 0.0f ) )
|
|
.AutoHeight()
|
|
[
|
|
PinnedCommands.ToSharedRef()
|
|
]
|
|
|
|
+ SVerticalBox::Slot()
|
|
.Padding( FMargin( 0.0f, 2.0f, 0.0f, 0.0f ) )
|
|
[
|
|
SAssignNew(TreeHolder, SOverlay)
|
|
]
|
|
]
|
|
];
|
|
|
|
|
|
SAssignNew(BlendProfilePicker, SBlendProfilePicker, GetEditableSkeleton())
|
|
.Standalone(true)
|
|
.OnBlendProfileSelected(this, &SSkeletonTree::OnBlendProfileSelected);
|
|
|
|
|
|
CreateTreeColumns();
|
|
|
|
SetInitialExpansionState();
|
|
|
|
OnLODSwitched();
|
|
}
|
|
|
|
PRAGMA_DISABLE_DEPRECATION_WARNINGS
|
|
SSkeletonTree::~SSkeletonTree()
|
|
{
|
|
if (EditableSkeleton.IsValid())
|
|
{
|
|
EditableSkeleton.Pin()->UnregisterOnSkeletonHierarchyChanged(this);
|
|
}
|
|
FCoreUObjectDelegates::OnPackageReloaded.RemoveAll(this);
|
|
}
|
|
PRAGMA_ENABLE_DEPRECATION_WARNINGS
|
|
|
|
void SSkeletonTree::BindCommands()
|
|
{
|
|
// This should not be called twice on the same instance
|
|
check( !UICommandList.IsValid() );
|
|
|
|
UICommandList = MakeShareable( new FUICommandList_Pinnable );
|
|
|
|
FUICommandList_Pinnable& CommandList = *UICommandList;
|
|
|
|
// Grab the list of menu commands to bind...
|
|
const FSkeletonTreeCommands& MenuActions = FSkeletonTreeCommands::Get();
|
|
|
|
// ...and bind them all
|
|
|
|
CommandList.MapAction(
|
|
MenuActions.FilteringFlattensHierarchy,
|
|
FExecuteAction::CreateLambda([this]() { GetMutableDefault<UPersonaOptions>()->bFlattenSkeletonHierarchyWhenFiltering = !GetDefault<UPersonaOptions>()->bFlattenSkeletonHierarchyWhenFiltering; ApplyFilter(); }),
|
|
FCanExecuteAction(),
|
|
FIsActionChecked::CreateLambda([]() { return GetDefault<UPersonaOptions>()->bFlattenSkeletonHierarchyWhenFiltering; }));
|
|
|
|
CommandList.MapAction(
|
|
MenuActions.HideParentsWhenFiltering,
|
|
FExecuteAction::CreateLambda([this]() { GetMutableDefault<UPersonaOptions>()->bHideParentsWhenFiltering = !GetDefault<UPersonaOptions>()->bHideParentsWhenFiltering; }),
|
|
FCanExecuteAction(),
|
|
FIsActionChecked::CreateLambda([]() { return GetDefault<UPersonaOptions>()->bHideParentsWhenFiltering; }));
|
|
|
|
CommandList.MapAction(
|
|
MenuActions.ShowBoneIndex,
|
|
FExecuteAction::CreateLambda([this]() { GetMutableDefault<UPersonaOptions>()->bShowBoneIndexes = !GetDefault<UPersonaOptions>()->bShowBoneIndexes; }),
|
|
FCanExecuteAction(),
|
|
FIsActionChecked::CreateLambda([]() { return GetDefault<UPersonaOptions>()->bShowBoneIndexes; }));
|
|
|
|
// Bone Filter commands
|
|
CommandList.BeginGroup(TEXT("BoneFilterGroup"));
|
|
|
|
CommandList.MapAction(
|
|
MenuActions.ShowAllBones,
|
|
FExecuteAction::CreateSP( this, &SSkeletonTree::SetBoneFilter, EBoneFilter::All ),
|
|
FCanExecuteAction(),
|
|
FIsActionChecked::CreateSP( this, &SSkeletonTree::IsBoneFilter, EBoneFilter::All ),
|
|
FIsActionButtonVisible::CreateSP( Builder.Get(), &ISkeletonTreeBuilder::IsShowingBones ));
|
|
|
|
CommandList.MapAction(
|
|
MenuActions.ShowMeshBones,
|
|
FExecuteAction::CreateSP( this, &SSkeletonTree::SetBoneFilter, EBoneFilter::Mesh ),
|
|
FCanExecuteAction(),
|
|
FIsActionChecked::CreateSP( this, &SSkeletonTree::IsBoneFilter, EBoneFilter::Mesh ),
|
|
FIsActionButtonVisible::CreateSP(Builder.Get(), &ISkeletonTreeBuilder::IsShowingBones));
|
|
|
|
CommandList.MapAction(
|
|
MenuActions.ShowLODBones,
|
|
FExecuteAction::CreateSP(this, &SSkeletonTree::SetBoneFilter, EBoneFilter::LOD),
|
|
FCanExecuteAction(),
|
|
FIsActionChecked::CreateSP(this, &SSkeletonTree::IsBoneFilter, EBoneFilter::LOD),
|
|
FIsActionButtonVisible::CreateSP(Builder.Get(), &ISkeletonTreeBuilder::IsShowingBones));
|
|
|
|
CommandList.MapAction(
|
|
MenuActions.ShowWeightedBones,
|
|
FExecuteAction::CreateSP( this, &SSkeletonTree::SetBoneFilter, EBoneFilter::Weighted ),
|
|
FCanExecuteAction(),
|
|
FIsActionChecked::CreateSP( this, &SSkeletonTree::IsBoneFilter, EBoneFilter::Weighted ),
|
|
FIsActionButtonVisible::CreateSP(Builder.Get(), &ISkeletonTreeBuilder::IsShowingBones));
|
|
|
|
CommandList.MapAction(
|
|
MenuActions.HideBones,
|
|
FExecuteAction::CreateSP( this, &SSkeletonTree::SetBoneFilter, EBoneFilter::None ),
|
|
FCanExecuteAction(),
|
|
FIsActionChecked::CreateSP( this, &SSkeletonTree::IsBoneFilter, EBoneFilter::None ),
|
|
FIsActionButtonVisible::CreateSP(Builder.Get(), &ISkeletonTreeBuilder::IsShowingBones));
|
|
|
|
CommandList.EndGroup();
|
|
|
|
// Socket filter commands
|
|
CommandList.BeginGroup(TEXT("SocketFilterGroup"));
|
|
|
|
CommandList.MapAction(
|
|
MenuActions.ShowActiveSockets,
|
|
FExecuteAction::CreateSP( this, &SSkeletonTree::SetSocketFilter, ESocketFilter::Active ),
|
|
FCanExecuteAction(),
|
|
FIsActionChecked::CreateSP( this, &SSkeletonTree::IsSocketFilter, ESocketFilter::Active ),
|
|
FIsActionButtonVisible::CreateSP(Builder.Get(), &ISkeletonTreeBuilder::IsShowingSockets ));
|
|
|
|
CommandList.MapAction(
|
|
MenuActions.ShowMeshSockets,
|
|
FExecuteAction::CreateSP( this, &SSkeletonTree::SetSocketFilter, ESocketFilter::Mesh ),
|
|
FCanExecuteAction(),
|
|
FIsActionChecked::CreateSP( this, &SSkeletonTree::IsSocketFilter, ESocketFilter::Mesh ),
|
|
FIsActionButtonVisible::CreateSP(Builder.Get(), &ISkeletonTreeBuilder::IsShowingSockets ));
|
|
|
|
CommandList.MapAction(
|
|
MenuActions.ShowSkeletonSockets,
|
|
FExecuteAction::CreateSP( this, &SSkeletonTree::SetSocketFilter, ESocketFilter::Skeleton ),
|
|
FCanExecuteAction(),
|
|
FIsActionChecked::CreateSP( this, &SSkeletonTree::IsSocketFilter, ESocketFilter::Skeleton ),
|
|
FIsActionButtonVisible::CreateSP(Builder.Get(), &ISkeletonTreeBuilder::IsShowingSockets ));
|
|
|
|
CommandList.MapAction(
|
|
MenuActions.ShowAllSockets,
|
|
FExecuteAction::CreateSP( this, &SSkeletonTree::SetSocketFilter, ESocketFilter::All ),
|
|
FCanExecuteAction(),
|
|
FIsActionChecked::CreateSP( this, &SSkeletonTree::IsSocketFilter, ESocketFilter::All ),
|
|
FIsActionButtonVisible::CreateSP(Builder.Get(), &ISkeletonTreeBuilder::IsShowingSockets ));
|
|
|
|
CommandList.MapAction(
|
|
MenuActions.HideSockets,
|
|
FExecuteAction::CreateSP( this, &SSkeletonTree::SetSocketFilter, ESocketFilter::None ),
|
|
FCanExecuteAction(),
|
|
FIsActionChecked::CreateSP( this, &SSkeletonTree::IsSocketFilter, ESocketFilter::None ),
|
|
FIsActionButtonVisible::CreateSP(Builder.Get(), &ISkeletonTreeBuilder::IsShowingSockets ));
|
|
|
|
CommandList.EndGroup();
|
|
|
|
CommandList.MapAction(
|
|
MenuActions.ShowRetargeting,
|
|
FExecuteAction::CreateSP(this, &SSkeletonTree::OnChangeShowingAdvancedOptions),
|
|
FCanExecuteAction(),
|
|
FIsActionChecked::CreateSP(this, &SSkeletonTree::IsShowingAdvancedOptions),
|
|
FIsActionButtonVisible::CreateLambda([this]() { return Builder->IsShowingBones() && bAllowSkeletonOperations; }));
|
|
|
|
CommandList.MapAction(
|
|
MenuActions.ShowDebugVisualization,
|
|
FExecuteAction::CreateSP(this, &SSkeletonTree::OnChangeShowingDebugVisualizationOptions),
|
|
FCanExecuteAction(),
|
|
FIsActionChecked::CreateSP(this, &SSkeletonTree::IsShowingDebugVisualizationOptions),
|
|
FIsActionButtonVisible::CreateLambda([this](){ return bShowDebugVisualizationOptions; }));
|
|
|
|
// Socket manipulation commands
|
|
CommandList.MapAction(
|
|
MenuActions.AddSocket,
|
|
FExecuteAction::CreateSP( this, &SSkeletonTree::OnAddSocket ),
|
|
FCanExecuteAction::CreateSP( this, &SSkeletonTree::IsAddingSocketsAllowed ) );
|
|
|
|
CommandList.MapAction(
|
|
FGenericCommands::Get().Rename,
|
|
FExecuteAction::CreateSP( this, &SSkeletonTree::OnRenameSelected ),
|
|
FCanExecuteAction::CreateSP( this, &SSkeletonTree::CanRenameSelected ) );
|
|
|
|
CommandList.MapAction(
|
|
MenuActions.CreateMeshSocket,
|
|
FExecuteAction::CreateSP( this, &SSkeletonTree::OnCustomizeSocket ) );
|
|
|
|
CommandList.MapAction(
|
|
MenuActions.RemoveMeshSocket,
|
|
FExecuteAction::CreateSP( this, &SSkeletonTree::OnDeleteSelectedRows ),
|
|
FCanExecuteAction::CreateSP( this, &SSkeletonTree::CanDeleteSelectedRows ) );
|
|
|
|
CommandList.MapAction(
|
|
MenuActions.PromoteSocketToSkeleton,
|
|
FExecuteAction::CreateSP( this, &SSkeletonTree::OnPromoteSocket ) ); // Adding customization just deletes the mesh socket
|
|
|
|
CommandList.MapAction(
|
|
MenuActions.DeleteSelectedRows,
|
|
FExecuteAction::CreateSP( this, &SSkeletonTree::OnDeleteSelectedRows ),
|
|
FCanExecuteAction::CreateSP( this, &SSkeletonTree::CanDeleteSelectedRows ));
|
|
|
|
CommandList.MapAction(
|
|
MenuActions.CopyBoneNames,
|
|
FExecuteAction::CreateSP( this, &SSkeletonTree::OnCopyBoneNames ));
|
|
|
|
static constexpr bool bSelectedOnly = true;
|
|
CommandList.MapAction(
|
|
MenuActions.ResetBoneTransforms,
|
|
FExecuteAction::CreateSP(this, &SSkeletonTree::OnResetBoneTransforms, bSelectedOnly ) );
|
|
|
|
CommandList.MapAction(
|
|
MenuActions.ResetAllBonesTransforms,
|
|
FExecuteAction::CreateSP(this, &SSkeletonTree::OnResetBoneTransforms, !bSelectedOnly ) );
|
|
|
|
CommandList.MapAction(
|
|
MenuActions.CopySockets,
|
|
FExecuteAction::CreateSP( this, &SSkeletonTree::OnCopySockets ),
|
|
FCanExecuteAction::CreateSP( this, &SSkeletonTree::CanCopySockets ));
|
|
|
|
CommandList.MapAction(
|
|
MenuActions.PasteSockets,
|
|
FExecuteAction::CreateSP( this, &SSkeletonTree::OnPasteSockets, false ),
|
|
FCanExecuteAction::CreateSP( this, &SSkeletonTree::CanPasteSockets ));
|
|
|
|
CommandList.MapAction(
|
|
MenuActions.PasteSocketsToSelectedBone,
|
|
FExecuteAction::CreateSP(this, &SSkeletonTree::OnPasteSockets, true),
|
|
FCanExecuteAction::CreateSP( this, &SSkeletonTree::CanPasteSockets ));
|
|
|
|
CommandList.MapAction(
|
|
MenuActions.FocusCamera,
|
|
FExecuteAction::CreateSP(this, &SSkeletonTree::HandleFocusCamera));
|
|
|
|
CommandList.MapAction(
|
|
MenuActions.CreateTimeBlendProfile,
|
|
FExecuteAction::CreateSP( this, &SSkeletonTree::OnCreateBlendProfile, EBlendProfileMode::TimeFactor));
|
|
|
|
CommandList.MapAction(
|
|
MenuActions.CreateWeightBlendProfile,
|
|
FExecuteAction::CreateSP(this, &SSkeletonTree::OnCreateBlendProfile, EBlendProfileMode::WeightFactor));
|
|
|
|
CommandList.MapAction(
|
|
MenuActions.CreateBlendMask,
|
|
FExecuteAction::CreateSP(this, &SSkeletonTree::OnCreateBlendProfile, EBlendProfileMode::BlendMask));
|
|
|
|
CommandList.MapAction(
|
|
MenuActions.DeleteCurrentBlendProfile,
|
|
FExecuteAction::CreateSP( this, &SSkeletonTree::OnDeleteCurrentBlendProfile));
|
|
|
|
CommandList.MapAction(
|
|
MenuActions.RenameBlendProfile,
|
|
FExecuteAction::CreateSP(this, &SSkeletonTree::OnRenameBlendProfile));
|
|
|
|
PinnedCommands->BindCommandList(UICommandList.ToSharedRef());
|
|
}
|
|
|
|
TSharedRef<ITableRow> SSkeletonTree::MakeTreeRowWidget(TSharedPtr<ISkeletonTreeItem> InInfo, const TSharedRef<STableViewBase>& OwnerTable)
|
|
{
|
|
check( InInfo.IsValid() );
|
|
|
|
return InInfo->MakeTreeRowWidget(OwnerTable, TAttribute<FText>::Create(TAttribute<FText>::FGetter::CreateLambda([this]() { return FilterText; })));
|
|
}
|
|
|
|
void SSkeletonTree::GetFilteredChildren(TSharedPtr<ISkeletonTreeItem> InInfo, TArray< TSharedPtr<ISkeletonTreeItem> >& OutChildren)
|
|
{
|
|
check(InInfo.IsValid());
|
|
OutChildren = InInfo->GetFilteredChildren();
|
|
}
|
|
|
|
/** Helper struct for when we rebuild the tree because of a change to its structure */
|
|
struct FScopedSavedSelection
|
|
{
|
|
FScopedSavedSelection(TSharedPtr<SSkeletonTree> InSkeletonTree)
|
|
: SkeletonTree(InSkeletonTree)
|
|
{
|
|
// record selected items
|
|
if (SkeletonTree.IsValid() && InSkeletonTree->SkeletonTreeView.IsValid())
|
|
{
|
|
TArray<TSharedPtr<ISkeletonTreeItem>> SelectedItems = InSkeletonTree->SkeletonTreeView->GetSelectedItems();
|
|
for (const TSharedPtr<ISkeletonTreeItem>& SelectedItem : SelectedItems)
|
|
{
|
|
SavedSelections.Add({ SelectedItem->GetRowItemName(), SelectedItem->GetTypeName(), SelectedItem->GetObject() });
|
|
}
|
|
}
|
|
}
|
|
|
|
~FScopedSavedSelection()
|
|
{
|
|
if (SkeletonTree.IsValid() && SkeletonTree->SkeletonTreeView.IsValid())
|
|
{
|
|
// restore selection
|
|
for (const TSharedPtr<ISkeletonTreeItem>& Item : SkeletonTree->LinearItems)
|
|
{
|
|
if (Item->GetFilterResult() != ESkeletonTreeFilterResult::Hidden)
|
|
{
|
|
for (FSavedSelection& SavedSelection : SavedSelections)
|
|
{
|
|
if (Item->GetRowItemName() == SavedSelection.ItemName && Item->GetTypeName() == SavedSelection.ItemType && Item->GetObject() == SavedSelection.ItemObject)
|
|
{
|
|
SkeletonTree->SkeletonTreeView->SetItemSelection(Item, true);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
struct FSavedSelection
|
|
{
|
|
/** Name of the selected item */
|
|
FName ItemName;
|
|
|
|
/** Type of the selected item */
|
|
FName ItemType;
|
|
|
|
/** Object of selected item */
|
|
UObject* ItemObject;
|
|
};
|
|
|
|
TSharedPtr<SSkeletonTree> SkeletonTree;
|
|
|
|
TArray<FSavedSelection> SavedSelections;
|
|
};
|
|
|
|
void SSkeletonTree::CreateTreeColumns()
|
|
{
|
|
TArray<FName> HiddenColumnsList;
|
|
HiddenColumnsList.Add(ISkeletonTree::Columns::Retargeting);
|
|
HiddenColumnsList.Add(ISkeletonTree::Columns::BlendProfile);
|
|
HiddenColumnsList.Add(ISkeletonTree::Columns::DebugVisualization);
|
|
|
|
TSharedRef<SHeaderRow> TreeHeaderRow =
|
|
SNew(SHeaderRow)
|
|
.CanSelectGeneratedColumn(true)
|
|
.HiddenColumnsList(HiddenColumnsList)
|
|
|
|
+ SHeaderRow::Column(ISkeletonTree::Columns::Name)
|
|
.ShouldGenerateWidget(true)
|
|
.DefaultLabel(LOCTEXT("SkeletonBoneNameLabel", "Name"))
|
|
.FillWidth(0.5f)
|
|
|
|
+ SHeaderRow::Column(ISkeletonTree::Columns::Retargeting)
|
|
.DefaultLabel(LOCTEXT("SkeletonBoneTranslationRetargetingLabel", "Translation Retargeting"))
|
|
.FillWidth(0.25f)
|
|
|
|
+ SHeaderRow::Column(ISkeletonTree::Columns::DebugVisualization)
|
|
.DefaultLabel(LOCTEXT("SkeletonBoneDebugVisualizationLabel", "Debug"))
|
|
.FillWidth(0.25f)
|
|
|
|
+ SHeaderRow::Column(ISkeletonTree::Columns::BlendProfile)
|
|
.DefaultLabel(LOCTEXT("BlendProfile", "Blend Profile"))
|
|
.FillWidth(0.25f)
|
|
.OnGetMenuContent(this, &SSkeletonTree::GetBlendProfileColumnMenuContent )
|
|
.HeaderContent()
|
|
[
|
|
SNew(SBox)
|
|
.HeightOverride(24.f)
|
|
.HAlign(HAlign_Left)
|
|
[
|
|
SAssignNew(BlendProfileHeader, SInlineEditableTextBlock)
|
|
.Text_Lambda([this] () -> FText
|
|
{
|
|
FName CurrentProfile = BlendProfilePicker->GetSelectedBlendProfileName();
|
|
return (CurrentProfile != NAME_None) ? FText::FromName(CurrentProfile) : LOCTEXT("NoBlendProfile", "NoBlend");
|
|
})
|
|
.OnTextCommitted_Lambda([this](const FText& InText, ETextCommit::Type InCommitType)
|
|
{
|
|
if (bIsCreateNewBlendProfile)
|
|
{
|
|
BlendProfilePicker->OnCreateNewProfileComitted(InText, InCommitType, NewBlendProfileMode);
|
|
bIsCreateNewBlendProfile = false;
|
|
}
|
|
else if(BlendProfilePicker->GetSelectedBlendProfileName() != NAME_None)
|
|
{
|
|
if (UBlendProfile* Profile = EditableSkeleton.Pin()->RenameBlendProfile(BlendProfilePicker->GetSelectedBlendProfileName(), FName(InText.ToString())))
|
|
{
|
|
BlendProfilePicker->SetSelectedProfile(Profile);
|
|
}
|
|
}
|
|
})
|
|
.OnVerifyTextChanged_Lambda([](const FText& InNewText, FText& OutErrorMessage) -> bool
|
|
{
|
|
return FName::IsValidXName(InNewText.ToString(), INVALID_OBJECTNAME_CHARACTERS INVALID_LONGPACKAGE_CHARACTERS, &OutErrorMessage);
|
|
})
|
|
.IsReadOnly(true)
|
|
]
|
|
];
|
|
|
|
{
|
|
FScopedSavedSelection ScopedSelection(SharedThis(this));
|
|
|
|
SkeletonTreeView = SNew(SSkeletonTreeView<TSharedPtr<ISkeletonTreeItem>>)
|
|
.TreeItemsSource(&FilteredItems)
|
|
.OnGenerateRow(this, &SSkeletonTree::MakeTreeRowWidget)
|
|
.OnGetChildren(this, &SSkeletonTree::GetFilteredChildren)
|
|
.OnContextMenuOpening(this, &SSkeletonTree::CreateContextMenu)
|
|
.OnSelectionChanged(this, &SSkeletonTree::OnSelectionChanged)
|
|
.OnIsSelectableOrNavigable(this, &SSkeletonTree::OnIsSelectableOrNavigable)
|
|
.OnItemScrolledIntoView(this, &SSkeletonTree::OnItemScrolledIntoView)
|
|
.OnMouseButtonDoubleClick(this, &SSkeletonTree::OnTreeDoubleClick)
|
|
.OnSetExpansionRecursive(this, &SSkeletonTree::SetTreeItemExpansionRecursive)
|
|
.HighlightParentNodesForSelection(true)
|
|
.HeaderRow
|
|
(
|
|
TreeHeaderRow
|
|
);
|
|
|
|
TreeHolder->ClearChildren();
|
|
TreeHolder->AddSlot()
|
|
[
|
|
SNew(SScrollBorder, SkeletonTreeView.ToSharedRef())
|
|
[
|
|
SkeletonTreeView.ToSharedRef()
|
|
]
|
|
];
|
|
}
|
|
|
|
CreateFromSkeleton();
|
|
}
|
|
|
|
void SSkeletonTree::CreateFromSkeleton()
|
|
{
|
|
// save selected items
|
|
FScopedSavedSelection ScopedSelection(SharedThis(this));
|
|
|
|
Items.Empty();
|
|
LinearItems.Empty();
|
|
FilteredItems.Empty();
|
|
|
|
FSkeletonTreeBuilderOutput Output(Items, LinearItems);
|
|
Builder->Build(Output);
|
|
ApplyFilter();
|
|
}
|
|
|
|
void SSkeletonTree::ApplyFilter()
|
|
{
|
|
TextFilterPtr->SetFilterText(FilterText);
|
|
|
|
FilteredItems.Empty();
|
|
|
|
FSkeletonTreeFilterArgs FilterArgs(!FilterText.IsEmpty() ? TextFilterPtr : nullptr);
|
|
FilterArgs.bFlattenHierarchyOnFilter = GetDefault<UPersonaOptions>()->bFlattenSkeletonHierarchyWhenFiltering;
|
|
Builder->Filter(FilterArgs, Items, FilteredItems);
|
|
|
|
if(!FilterText.IsEmpty())
|
|
{
|
|
for (TSharedPtr<ISkeletonTreeItem>& Item : LinearItems)
|
|
{
|
|
if (Item->GetFilterResult() > ESkeletonTreeFilterResult::Hidden)
|
|
{
|
|
SkeletonTreeView->SetItemExpansion(Item, true);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
SetInitialExpansionState();
|
|
}
|
|
|
|
HandleTreeRefresh();
|
|
}
|
|
|
|
void SSkeletonTree::SetInitialExpansionState()
|
|
{
|
|
for (TSharedPtr<ISkeletonTreeItem>& Item : LinearItems)
|
|
{
|
|
SkeletonTreeView->SetItemExpansion(Item, Item->IsInitiallyExpanded());
|
|
}
|
|
}
|
|
|
|
TSharedPtr< SWidget > SSkeletonTree::CreateContextMenu()
|
|
{
|
|
const FSkeletonTreeCommands& Actions = FSkeletonTreeCommands::Get();
|
|
|
|
TArray<TSharedPtr<ISkeletonTreeItem>> SelectedItems = SkeletonTreeView->GetSelectedItems();
|
|
FSkeletonTreeSelection BoneTreeSelection(SelectedItems);
|
|
|
|
const bool CloseAfterSelection = true;
|
|
FMenuBuilder MenuBuilder( CloseAfterSelection, UICommandList, Extenders );
|
|
{
|
|
if(BoneTreeSelection.HasSelectedOfType<FSkeletonTreeAttachedAssetItem>() || BoneTreeSelection.HasSelectedOfType<FSkeletonTreeSocketItem>() || BoneTreeSelection.HasSelectedOfType<FSkeletonTreeVirtualBoneItem>())
|
|
{
|
|
MenuBuilder.BeginSection("SkeletonTreeSelectedItemsActions", LOCTEXT( "SelectedActions", "Selected Item Actions" ) );
|
|
MenuBuilder.AddMenuEntry( Actions.DeleteSelectedRows );
|
|
MenuBuilder.EndSection();
|
|
}
|
|
|
|
|
|
const bool bNeedsBoneActionsHeading = BoneTreeSelection.HasSelectedOfType<FSkeletonTreeBoneItem>() || BoneTreeSelection.HasSelectedOfType<FSkeletonTreeVirtualBoneItem>();
|
|
|
|
if (bNeedsBoneActionsHeading)
|
|
{
|
|
MenuBuilder.BeginSection("SkeletonTreeBonesAction", LOCTEXT("BoneActions", "Selected Bone Actions"));
|
|
}
|
|
|
|
const bool bHasBoneSelected = BoneTreeSelection.HasSelectedOfType<FSkeletonTreeBoneItem>();
|
|
const bool bHasVirtualBoneSelected = BoneTreeSelection.HasSelectedOfType<FSkeletonTreeVirtualBoneItem>();
|
|
if (bHasBoneSelected || bHasVirtualBoneSelected)
|
|
{
|
|
MenuBuilder.AddMenuEntry(Actions.CopyBoneNames);
|
|
|
|
if (bHasBoneSelected)
|
|
{
|
|
MenuBuilder.AddMenuEntry(Actions.ResetBoneTransforms);
|
|
}
|
|
|
|
if (BoneTreeSelection.IsSingleOfTypesSelected<FSkeletonTreeBoneItem, FSkeletonTreeVirtualBoneItem>() && bAllowSkeletonOperations)
|
|
{
|
|
MenuBuilder.AddMenuEntry(Actions.AddSocket);
|
|
MenuBuilder.AddMenuEntry(Actions.PasteSockets);
|
|
MenuBuilder.AddMenuEntry(Actions.PasteSocketsToSelectedBone);
|
|
}
|
|
}
|
|
|
|
if (bNeedsBoneActionsHeading)
|
|
{
|
|
if (bAllowSkeletonOperations)
|
|
{
|
|
MenuBuilder.AddSubMenu(LOCTEXT("AddVirtualBone", "Add Virtual Bone"),
|
|
LOCTEXT("AddVirtualBone_ToolTip", "Adds a virtual bone to the skeleton."),
|
|
FNewMenuDelegate::CreateLambda([this](FMenuBuilder& MenuBuilder)
|
|
{
|
|
TSharedRef<SBoneTreeMenu> MenuContent = SSkeletonTree::CreateVirtualBoneMenu(this);
|
|
MenuBuilder.AddWidget(MenuContent, FText::GetEmpty(), true);
|
|
}));
|
|
}
|
|
|
|
MenuBuilder.EndSection();
|
|
}
|
|
|
|
if (bAllowSkeletonOperations)
|
|
{
|
|
if(BoneTreeSelection.HasSelectedOfType<FSkeletonTreeBoneItem>())
|
|
{
|
|
UBlendProfile* const SelectedBlendProfile = BlendProfilePicker->GetSelectedBlendProfile();
|
|
if(SelectedBlendProfile && BoneTreeSelection.IsSingleOfTypeSelected<FSkeletonTreeBoneItem>())
|
|
{
|
|
TSharedPtr<FSkeletonTreeBoneItem> BoneItem = BoneTreeSelection.GetSelectedItems<FSkeletonTreeBoneItem>()[0];
|
|
|
|
FName BoneName = BoneItem->GetAttachName();
|
|
const USkeleton& Skeleton = GetEditableSkeletonInternal()->GetSkeleton();
|
|
int32 BoneIndex = Skeleton.GetReferenceSkeleton().FindBoneIndex(BoneName);
|
|
|
|
float CurrentBlendScale = SelectedBlendProfile->GetBoneBlendScale(BoneIndex);
|
|
|
|
MenuBuilder.BeginSection("SkeletonTreeBlendProfileScales", LOCTEXT("BlendProfileContextOptions", "Blend Profile"));
|
|
{
|
|
FUIAction RecursiveSetScales;
|
|
RecursiveSetScales.ExecuteAction = FExecuteAction::CreateSP(this, &SSkeletonTree::RecursiveSetBlendProfileScales, CurrentBlendScale);
|
|
|
|
MenuBuilder.AddMenuEntry
|
|
(
|
|
FText::Format(LOCTEXT("RecursiveSetBlendScales_Label", "Recursively Set Blend Scales To {0}"), FText::AsNumber(CurrentBlendScale)),
|
|
LOCTEXT("RecursiveSetBlendScales_ToolTip", "Sets all child bones to use the same blend profile scale as the selected bone"),
|
|
FSlateIcon(),
|
|
RecursiveSetScales
|
|
);
|
|
}
|
|
MenuBuilder.EndSection();
|
|
}
|
|
|
|
if(IsShowingAdvancedOptions())
|
|
{
|
|
MenuBuilder.BeginSection("SkeletonTreeBoneTranslationRetargeting", LOCTEXT("BoneTranslationRetargetingHeader", "Bone Translation Retargeting"));
|
|
{
|
|
FUIAction RecursiveRetargetingSkeletonAction = FUIAction(FExecuteAction::CreateSP(this, &SSkeletonTree::SetBoneTranslationRetargetingModeRecursive, EBoneTranslationRetargetingMode::Skeleton));
|
|
FUIAction RecursiveRetargetingAnimationAction = FUIAction(FExecuteAction::CreateSP(this, &SSkeletonTree::SetBoneTranslationRetargetingModeRecursive, EBoneTranslationRetargetingMode::Animation));
|
|
FUIAction RecursiveRetargetingAnimationScaledAction = FUIAction(FExecuteAction::CreateSP(this, &SSkeletonTree::SetBoneTranslationRetargetingModeRecursive, EBoneTranslationRetargetingMode::AnimationScaled));
|
|
FUIAction RecursiveRetargetingAnimationRelativeAction = FUIAction(FExecuteAction::CreateSP(this, &SSkeletonTree::SetBoneTranslationRetargetingModeRecursive, EBoneTranslationRetargetingMode::AnimationRelative));
|
|
FUIAction RecursiveRetargetingOrientAndScaleAction = FUIAction(FExecuteAction::CreateSP(this, &SSkeletonTree::SetBoneTranslationRetargetingModeRecursive, EBoneTranslationRetargetingMode::OrientAndScale));
|
|
|
|
MenuBuilder.AddMenuEntry
|
|
(LOCTEXT("SetTranslationRetargetingSkeletonChildrenAction", "Recursively Set Translation Retargeting Skeleton")
|
|
, LOCTEXT("BoneTranslationRetargetingSkeletonToolTip", "Use translation from Skeleton.")
|
|
, FSlateIcon()
|
|
, RecursiveRetargetingSkeletonAction
|
|
);
|
|
|
|
MenuBuilder.AddMenuEntry
|
|
(LOCTEXT("SetTranslationRetargetingAnimationChildrenAction", "Recursively Set Translation Retargeting Animation")
|
|
, LOCTEXT("BoneTranslationRetargetingAnimationToolTip", "Use translation from animation.")
|
|
, FSlateIcon()
|
|
, RecursiveRetargetingAnimationAction
|
|
);
|
|
|
|
MenuBuilder.AddMenuEntry
|
|
(LOCTEXT("SetTranslationRetargetingAnimationScaledChildrenAction", "Recursively Set Translation Retargeting AnimationScaled")
|
|
, LOCTEXT("BoneTranslationRetargetingAnimationScaledToolTip", "Use translation from animation, scale length by Skeleton's proportions.")
|
|
, FSlateIcon()
|
|
, RecursiveRetargetingAnimationScaledAction
|
|
);
|
|
|
|
MenuBuilder.AddMenuEntry
|
|
(LOCTEXT("SetTranslationRetargetingAnimationRelativeChildrenAction", "Recursively Set Translation Retargeting AnimationRelative")
|
|
, LOCTEXT("BoneTranslationRetargetingAnimationRelativeToolTip", "Use relative translation from animation similar to an additive animation.")
|
|
, FSlateIcon()
|
|
, RecursiveRetargetingAnimationRelativeAction
|
|
);
|
|
|
|
MenuBuilder.AddMenuEntry
|
|
(LOCTEXT("SetTranslationRetargetingOrientAndScaleChildrenAction", "Recursively Set Translation Retargeting OrientAndScale")
|
|
, LOCTEXT("BoneTranslationRetargetingOrientAndScaleToolTip", "Orient And Scale Translation.")
|
|
, FSlateIcon()
|
|
, RecursiveRetargetingOrientAndScaleAction
|
|
);
|
|
}
|
|
MenuBuilder.EndSection();
|
|
}
|
|
}
|
|
}
|
|
|
|
if(bAllowMeshOperations)
|
|
{
|
|
MenuBuilder.BeginSection("SkeletonTreeBoneReductionForLOD", LOCTEXT("BoneReductionHeader", "LOD Bone Reduction"));
|
|
{
|
|
MenuBuilder.AddSubMenu(
|
|
LOCTEXT("SkeletonTreeBoneReductionForLOD_RemoveSelectedFromLOD", "Remove Selected..."),
|
|
FText::GetEmpty(),
|
|
FNewMenuDelegate::CreateStatic(&SSkeletonTree::CreateMenuForBoneReduction, this, LastCachedLODForPreviewMeshComponent, true)
|
|
);
|
|
|
|
MenuBuilder.AddSubMenu(
|
|
LOCTEXT("SkeletonTreeBoneReductionForLOD_RemoveChildrenFromLOD", "Remove Children..."),
|
|
FText::GetEmpty(),
|
|
FNewMenuDelegate::CreateStatic(&SSkeletonTree::CreateMenuForBoneReduction, this, LastCachedLODForPreviewMeshComponent, false)
|
|
);
|
|
}
|
|
MenuBuilder.EndSection();
|
|
}
|
|
|
|
if(bAllowSkeletonOperations)
|
|
{
|
|
if (BoneTreeSelection.HasSelectedOfType<FSkeletonTreeVirtualBoneItem>())
|
|
{
|
|
MenuBuilder.BeginSection("SkeletonTreeVirtualBoneActions", LOCTEXT("VirtualBoneActions", "Selected Virtual Bone Actions"));
|
|
|
|
if (BoneTreeSelection.IsSingleOfTypeSelected<FSkeletonTreeVirtualBoneItem>())
|
|
{
|
|
MenuBuilder.AddMenuEntry(FGenericCommands::Get().Rename, NAME_None, LOCTEXT("RenameVirtualBone_Label", "Rename Virtual Bone"), LOCTEXT("RenameVirtualBone_Tooltip", "Rename this virtual bone"));
|
|
}
|
|
|
|
MenuBuilder.EndSection();
|
|
}
|
|
|
|
if(BoneTreeSelection.HasSelectedOfType<FSkeletonTreeSocketItem>())
|
|
{
|
|
MenuBuilder.BeginSection("SkeletonTreeSocketsActions", LOCTEXT( "SocketActions", "Selected Socket Actions" ) );
|
|
|
|
MenuBuilder.AddMenuEntry( Actions.CopySockets );
|
|
|
|
if(BoneTreeSelection.IsSingleOfTypeSelected<FSkeletonTreeSocketItem>())
|
|
{
|
|
MenuBuilder.AddMenuEntry( FGenericCommands::Get().Rename, NAME_None, LOCTEXT("RenameSocket_Label", "Rename Socket"), LOCTEXT("RenameSocket_Tooltip", "Rename this socket") );
|
|
|
|
TSharedPtr<FSkeletonTreeSocketItem> SocketItem = BoneTreeSelection.GetSelectedItems<FSkeletonTreeSocketItem>()[0];
|
|
|
|
if (SocketItem->IsSocketCustomized() && SocketItem->GetParentType() == ESocketParentType::Mesh )
|
|
{
|
|
MenuBuilder.AddMenuEntry( Actions.RemoveMeshSocket );
|
|
}
|
|
|
|
// If the socket is on the skeleton, we have a valid mesh
|
|
// and there isn't one of the same name on the mesh, we can customize it
|
|
if (SocketItem->CanCustomizeSocket() )
|
|
{
|
|
if (SocketItem->GetParentType() == ESocketParentType::Skeleton )
|
|
{
|
|
MenuBuilder.AddMenuEntry( Actions.CreateMeshSocket );
|
|
}
|
|
else if (SocketItem->GetParentType() == ESocketParentType::Mesh )
|
|
{
|
|
// If a socket is on the mesh only, then offer to promote it to the skeleton
|
|
MenuBuilder.AddMenuEntry( Actions.PromoteSocketToSkeleton );
|
|
}
|
|
}
|
|
}
|
|
|
|
MenuBuilder.EndSection();
|
|
}
|
|
}
|
|
|
|
if (BoneTreeSelection.HasSelectedOfType<FSkeletonTreeBoneItem>() || BoneTreeSelection.HasSelectedOfType<FSkeletonTreeSocketItem>())
|
|
{
|
|
MenuBuilder.BeginSection("SkeletonTreeAttachedAssets", LOCTEXT( "AttachedAssetsActionsHeader", "Attached Assets Actions" ) );
|
|
|
|
if ( BoneTreeSelection.IsSingleItemSelected() )
|
|
{
|
|
MenuBuilder.AddSubMenu( LOCTEXT( "AttachNewAsset", "Add Preview Asset" ),
|
|
LOCTEXT ( "AttachNewAsset_ToolTip", "Attaches an asset to this part of the skeleton. Assets can also be dragged onto the skeleton from a content browser to attach" ),
|
|
FNewMenuDelegate::CreateSP( this, &SSkeletonTree::FillAttachAssetSubmenu, BoneTreeSelection.GetSingleSelectedItem() ) );
|
|
}
|
|
|
|
FUIAction RemoveAllAttachedAssets = FUIAction( FExecuteAction::CreateSP( this, &SSkeletonTree::OnRemoveAllAssets ),
|
|
FCanExecuteAction::CreateSP( this, &SSkeletonTree::CanRemoveAllAssets ));
|
|
|
|
MenuBuilder.AddMenuEntry( LOCTEXT( "RemoveAllAttachedAssets", "Remove All Attached Assets" ),
|
|
LOCTEXT ( "RemoveAllAttachedAssets_ToolTip", "Removes all the attached assets from the skeleton and mesh." ),
|
|
FSlateIcon(), RemoveAllAttachedAssets );
|
|
|
|
MenuBuilder.EndSection();
|
|
}
|
|
|
|
// Add an empty section so the menu can be extended when there are no optionally-added entries
|
|
MenuBuilder.BeginSection("SkeletonTreeContextMenu");
|
|
MenuBuilder.EndSection();
|
|
}
|
|
|
|
return MenuBuilder.MakeWidget();
|
|
}
|
|
|
|
bool GetSourceNameFromItem(TSharedPtr<ISkeletonTreeItem> SourceBone, FName& OutName)
|
|
{
|
|
if (SourceBone->IsOfType<FSkeletonTreeBoneItem>())
|
|
{
|
|
OutName = SourceBone->GetRowItemName();
|
|
return true;
|
|
}
|
|
if (SourceBone->IsOfType<FSkeletonTreeVirtualBoneItem>())
|
|
{
|
|
OutName = SourceBone->GetRowItemName();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
TSharedRef<SBoneTreeMenu> SSkeletonTree::CreateVirtualBoneMenu(SSkeletonTree* InSkeletonTree)
|
|
{
|
|
const TArray<TSharedPtr<ISkeletonTreeItem>> SelectedItems = InSkeletonTree->GetSelectedItems();
|
|
|
|
TSharedRef<SBoneTreeMenu> MenuContent = SNew(SBoneTreeMenu)
|
|
.bShowVirtualBones(false)
|
|
.Title(LOCTEXT("TargetBonePickerTitle", "Pick Target Bone..."))
|
|
.OnBoneSelectionChanged(InSkeletonTree, &SSkeletonTree::OnVirtualTargetBonePicked, SelectedItems)
|
|
.OnGetReferenceSkeleton(InSkeletonTree, &SSkeletonTree::OnGetReferenceSkeleton);
|
|
|
|
MenuContent->RegisterActiveTimer(0.0f, FWidgetActiveTimerDelegate::CreateLambda(
|
|
[FilterTextBox = MenuContent->GetFilterTextWidget()](double, float)
|
|
{
|
|
FSlateApplication::Get().SetKeyboardFocus(FilterTextBox);
|
|
return EActiveTimerReturnType::Stop;
|
|
}));
|
|
|
|
return MenuContent;
|
|
}
|
|
|
|
void SSkeletonTree::OnVirtualTargetBonePicked(FName TargetBoneName, TArray<TSharedPtr<ISkeletonTreeItem>> SourceBones)
|
|
{
|
|
FSlateApplication::Get().DismissAllMenus();
|
|
|
|
TArray<FName> VirtualBoneNames;
|
|
|
|
for (const TSharedPtr<ISkeletonTreeItem>& SourceBone : SourceBones)
|
|
{
|
|
FName SourceBoneName;
|
|
if(GetSourceNameFromItem(SourceBone, SourceBoneName))
|
|
{
|
|
FName NewVirtualBoneName;
|
|
if(!GetEditableSkeletonInternal()->HandleAddVirtualBone(SourceBoneName, TargetBoneName, NewVirtualBoneName))
|
|
{
|
|
UE_LOG(LogAnimation, Log, TEXT("Could not create space switch bone from %s to %s, it already exists"), *SourceBoneName.ToString(), *TargetBoneName.ToString());
|
|
}
|
|
else
|
|
{
|
|
VirtualBoneNames.Add(NewVirtualBoneName);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (VirtualBoneNames.Num() > 0)
|
|
{
|
|
CreateFromSkeleton();
|
|
SkeletonTreeView->ClearSelection();
|
|
|
|
TSharedPtr<ISkeletonTreeItem> LastItem;
|
|
for (TSharedPtr<ISkeletonTreeItem> SkeletonRow : LinearItems)
|
|
{
|
|
if (SkeletonRow->IsOfType<FSkeletonTreeVirtualBoneItem>())
|
|
{
|
|
LastItem = SkeletonRow;
|
|
FName RowName = SkeletonRow->GetRowItemName();
|
|
for (const FName& VB : VirtualBoneNames)
|
|
{
|
|
if (RowName == VB)
|
|
{
|
|
SkeletonTreeView->SetItemSelection(SkeletonRow, true);
|
|
SkeletonTreeView->RequestScrollIntoView(SkeletonRow);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (LastItem.IsValid())
|
|
{
|
|
SkeletonTreeView->RequestScrollIntoView(LastItem);
|
|
}
|
|
}
|
|
}
|
|
|
|
void SSkeletonTree::CreateMenuForBoneReduction(FMenuBuilder& MenuBuilder, SSkeletonTree * Widget, int32 LODIndex, bool bIncludeSelected)
|
|
{
|
|
MenuBuilder.AddMenuEntry
|
|
(FText::FromString(FString::Printf(TEXT("From LOD %d and below"), LODIndex))
|
|
, FText::FromString(FString::Printf(TEXT("Remove Selected %s from current LOD %d and all lower LODs"), (bIncludeSelected) ? TEXT("bones") : TEXT("children"), LODIndex))
|
|
, FSlateIcon()
|
|
, FUIAction(FExecuteAction::CreateSP(Widget, &SSkeletonTree::RemoveFromLOD, LODIndex, bIncludeSelected, true))
|
|
);
|
|
|
|
MenuBuilder.AddMenuEntry
|
|
(FText::FromString(FString::Printf(TEXT("From LOD %d only"), LODIndex))
|
|
, FText::FromString(FString::Printf(TEXT("Remove selected %s from current LOD %d only"), (bIncludeSelected) ? TEXT("bones") : TEXT("children"), LODIndex))
|
|
, FSlateIcon()
|
|
, FUIAction(FExecuteAction::CreateSP(Widget, &SSkeletonTree::RemoveFromLOD, LODIndex, bIncludeSelected, false))
|
|
);
|
|
}
|
|
|
|
|
|
void SSkeletonTree::SetBoneTranslationRetargetingModeRecursive(EBoneTranslationRetargetingMode::Type NewRetargetingMode)
|
|
{
|
|
TArray<FName> BoneNames;
|
|
TArray<TSharedPtr<ISkeletonTreeItem>> SelectedItems = SkeletonTreeView->GetSelectedItems();
|
|
FSkeletonTreeSelection TreeSelection(SelectedItems);
|
|
for (const TSharedPtr<FSkeletonTreeBoneItem>& Item : TreeSelection.GetSelectedItems<FSkeletonTreeBoneItem>())
|
|
{
|
|
BoneNames.Add(Item->GetRowItemName());
|
|
}
|
|
|
|
GetEditableSkeletonInternal()->SetBoneTranslationRetargetingModeRecursive(BoneNames, NewRetargetingMode);
|
|
}
|
|
|
|
void SSkeletonTree::RemoveFromLOD(int32 LODIndex, bool bIncludeSelected, bool bIncludeBelowLODs)
|
|
{
|
|
// we cant do this without a preview scene
|
|
if (!GetPreviewScene().IsValid())
|
|
{
|
|
return;
|
|
}
|
|
|
|
UDebugSkelMeshComponent* PreviewMeshComponent = GetPreviewScene()->GetPreviewMeshComponent();
|
|
if (!PreviewMeshComponent->GetSkeletalMeshAsset())
|
|
{
|
|
return;
|
|
}
|
|
|
|
// ask users you can't undo this change, and warn them
|
|
const FText Message(LOCTEXT("RemoveBonesFromLODWarning", "This action can't be undone. Would you like to continue?"));
|
|
if (FMessageDialog::Open(EAppMsgType::YesNo, Message) == EAppReturnType::Yes)
|
|
{
|
|
TArray<TSharedPtr<ISkeletonTreeItem>> SelectedItems = SkeletonTreeView->GetSelectedItems();
|
|
FSkeletonTreeSelection TreeSelection(SelectedItems);
|
|
const FReferenceSkeleton& RefSkeleton = GetEditableSkeletonInternal()->GetSkeleton().GetReferenceSkeleton();
|
|
|
|
TArray<FName> BonesToRemove;
|
|
|
|
//Scoped post edit change
|
|
{
|
|
FScopedSkeletalMeshPostEditChange ScopedPostEditChange(PreviewMeshComponent->GetSkeletalMeshAsset());
|
|
|
|
for (const TSharedPtr<FSkeletonTreeBoneItem>& Item : TreeSelection.GetSelectedItems<FSkeletonTreeBoneItem>())
|
|
{
|
|
FName BoneName = Item->GetRowItemName();
|
|
int32 BoneIndex = RefSkeleton.FindBoneIndex(BoneName);
|
|
if (BoneIndex != INDEX_NONE)
|
|
{
|
|
if (bIncludeSelected)
|
|
{
|
|
PreviewMeshComponent->GetSkeletalMeshAsset()->AddBoneToReductionSetting(LODIndex, BoneName);
|
|
BonesToRemove.AddUnique(BoneName);
|
|
}
|
|
else
|
|
{
|
|
for (int32 ChildIndex = BoneIndex + 1; ChildIndex < RefSkeleton.GetRawBoneNum(); ++ChildIndex)
|
|
{
|
|
if (RefSkeleton.GetParentIndex(ChildIndex) == BoneIndex)
|
|
{
|
|
FName ChildBoneName = RefSkeleton.GetBoneName(ChildIndex);
|
|
PreviewMeshComponent->GetSkeletalMeshAsset()->AddBoneToReductionSetting(LODIndex, ChildBoneName);
|
|
BonesToRemove.AddUnique(ChildBoneName);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
int32 TotalLOD = PreviewMeshComponent->GetSkeletalMeshAsset()->GetLODNum();
|
|
IMeshUtilities& MeshUtilities = FModuleManager::Get().LoadModuleChecked<IMeshUtilities>("MeshUtilities");
|
|
|
|
if (bIncludeBelowLODs)
|
|
{
|
|
for (int32 Index = LODIndex + 1; Index < TotalLOD; ++Index)
|
|
{
|
|
PreviewMeshComponent->GetSkeletalMeshAsset()->AddBoneToReductionSetting(Index, BonesToRemove);
|
|
// We don't pass BoneNamesToRemove, as AddBoneToReductionSetting has added them to the LODInfoArray[LODIndex].BonesToRemove
|
|
// Which will be used by RemoveBonesFromMesh if we pass null BonesNamesToRemove (else will just remove the newly deleted bones, which is wrong)
|
|
MeshUtilities.RemoveBonesFromMesh(PreviewMeshComponent->GetSkeletalMeshAsset(), Index, nullptr);
|
|
}
|
|
}
|
|
|
|
// remove from current LOD
|
|
// We don't pass BoneNamesToRemove, as AddBoneToReductionSetting has added them to the LODInfoArray[LODIndex].BonesToRemove
|
|
// Which will be used by RemoveBonesFromMesh if we pass null BonesNamesToRemove (else will just remove the newly deleted bones, which is wrong)
|
|
MeshUtilities.RemoveBonesFromMesh(PreviewMeshComponent->GetSkeletalMeshAsset(), LODIndex, nullptr);
|
|
}
|
|
// update UI to reflect the change
|
|
OnLODSwitched();
|
|
}
|
|
}
|
|
|
|
void SSkeletonTree::OnCopyBoneNames()
|
|
{
|
|
TArray<TSharedPtr<ISkeletonTreeItem>> SelectedItems = SkeletonTreeView->GetSelectedItems();
|
|
FSkeletonTreeSelection TreeSelection(SelectedItems);
|
|
|
|
TArray<TSharedPtr<ISkeletonTreeItem>> SelectedBones = TreeSelection.GetSelectedItemsOfTypes<FSkeletonTreeBoneItem, FSkeletonTreeVirtualBoneItem>();
|
|
|
|
if( SelectedBones.Num() > 0 )
|
|
{
|
|
bool bFirst = true;
|
|
FString BoneNames;
|
|
for (const TSharedPtr<ISkeletonTreeItem>& Item : SelectedBones)
|
|
{
|
|
FName BoneName = Item->GetRowItemName();
|
|
if (!bFirst)
|
|
{
|
|
BoneNames += "\r\n";
|
|
}
|
|
BoneNames += BoneName.ToString();
|
|
bFirst = false;
|
|
}
|
|
FPlatformApplicationMisc::ClipboardCopy( *BoneNames );
|
|
}
|
|
}
|
|
|
|
void SSkeletonTree::OnResetBoneTransforms(const bool bSelectedOnly)
|
|
{
|
|
if (GetPreviewScene().IsValid())
|
|
{
|
|
UDebugSkelMeshComponent* PreviewComponent = GetPreviewScene()->GetPreviewMeshComponent();
|
|
check(PreviewComponent);
|
|
UAnimPreviewInstance* PreviewInstance = PreviewComponent->PreviewInstance;
|
|
check(PreviewInstance);
|
|
|
|
const TArray<FAnimNode_ModifyBone>& ModifyBones = PreviewInstance->GetBoneControllers();
|
|
if (ModifyBones.IsEmpty())
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!bSelectedOnly)
|
|
{
|
|
GEditor->BeginTransaction(LOCTEXT("SkeletonTree_ResetAllBonesTransforms", "Reset All Bone Transforms"));
|
|
{
|
|
PreviewInstance->SetFlags(RF_Transactional);
|
|
PreviewInstance->Modify();
|
|
PreviewInstance->ResetModifiedBone();
|
|
}
|
|
GEditor->EndTransaction();
|
|
return;
|
|
}
|
|
|
|
TArray<TSharedPtr<ISkeletonTreeItem>> SelectedItems = SkeletonTreeView->GetSelectedItems();
|
|
FSkeletonTreeSelection TreeSelection(SelectedItems);
|
|
TArray<TSharedPtr<FSkeletonTreeBoneItem>> SelectedBones = TreeSelection.GetSelectedItems<FSkeletonTreeBoneItem>();
|
|
if (SelectedBones.Num() > 0)
|
|
{
|
|
bool bModified = false;
|
|
GEditor->BeginTransaction(LOCTEXT("SkeletonTree_ResetBoneTransforms", "Reset Bone Transforms"));
|
|
|
|
for (const TSharedPtr<FSkeletonTreeBoneItem>& Item : SelectedBones)
|
|
{
|
|
FName BoneName = Item->GetRowItemName();
|
|
const FAnimNode_ModifyBone* ModifiedBone = PreviewInstance->FindModifiedBone(BoneName);
|
|
if (ModifiedBone != nullptr)
|
|
{
|
|
if (!bModified)
|
|
{
|
|
PreviewInstance->SetFlags(RF_Transactional);
|
|
PreviewInstance->Modify();
|
|
bModified = true;
|
|
}
|
|
|
|
PreviewInstance->RemoveBoneModification(BoneName);
|
|
}
|
|
}
|
|
|
|
GEditor->EndTransaction();
|
|
}
|
|
}
|
|
}
|
|
|
|
void SSkeletonTree::OnCopySockets() const
|
|
{
|
|
TArray<TSharedPtr<ISkeletonTreeItem>> SelectedItems = SkeletonTreeView->GetSelectedItems();
|
|
FSkeletonTreeSelection TreeSelection(SelectedItems);
|
|
TArray<TSharedPtr<FSkeletonTreeSocketItem>> SelectedSockets = TreeSelection.GetSelectedItems<FSkeletonTreeSocketItem>();
|
|
int32 NumSocketsToCopy = SelectedSockets.Num();
|
|
if ( NumSocketsToCopy > 0 )
|
|
{
|
|
FString SocketsDataString;
|
|
|
|
for (const TSharedPtr<FSkeletonTreeSocketItem>& Item : SelectedSockets)
|
|
{
|
|
SocketsDataString += SerializeSocketToString(Item->GetSocket(), Item->GetParentType());
|
|
}
|
|
|
|
FString CopyString = FString::Printf( TEXT("%s\nNumSockets=%d\n%s"), *FEditableSkeleton::SocketCopyPasteHeader, NumSocketsToCopy, *SocketsDataString );
|
|
|
|
FPlatformApplicationMisc::ClipboardCopy( *CopyString );
|
|
}
|
|
}
|
|
|
|
bool SSkeletonTree::CanCopySockets() const
|
|
{
|
|
TArray<TSharedPtr<ISkeletonTreeItem>> SelectedItems = SkeletonTreeView->GetSelectedItems();
|
|
FSkeletonTreeSelection TreeSelection(SelectedItems);
|
|
TArray<TSharedPtr<FSkeletonTreeSocketItem>> SelectedSockets = TreeSelection.GetSelectedItems<FSkeletonTreeSocketItem>();
|
|
return SelectedSockets.Num() > 0;
|
|
}
|
|
|
|
FString SSkeletonTree::SerializeSocketToString( USkeletalMeshSocket* Socket, ESocketParentType ParentType) const
|
|
{
|
|
FString SocketString;
|
|
|
|
SocketString += FString::Printf( TEXT( "IsOnSkeleton=%s\n" ), ParentType == ESocketParentType::Skeleton ? TEXT( "1" ) : TEXT( "0" ) );
|
|
|
|
FStringOutputDevice Buffer;
|
|
const FExportObjectInnerContext Context;
|
|
UExporter::ExportToOutputDevice( &Context, Socket, nullptr, Buffer, TEXT( "copy" ), 0, PPF_Copy, false );
|
|
SocketString += Buffer;
|
|
|
|
return SocketString;
|
|
}
|
|
|
|
void SSkeletonTree::OnPasteSockets(bool bPasteToSelectedBone)
|
|
{
|
|
TArray<TSharedPtr<ISkeletonTreeItem>> SelectedItems = SkeletonTreeView->GetSelectedItems();
|
|
FSkeletonTreeSelection TreeSelection(SelectedItems);
|
|
|
|
// Pasting sockets should only work if there is just one bone selected
|
|
if ( TreeSelection.IsSingleOfTypesSelected<FSkeletonTreeBoneItem, FSkeletonTreeVirtualBoneItem>())
|
|
{
|
|
FName DestBoneName = bPasteToSelectedBone ? TreeSelection.GetSingleSelectedItem()->GetRowItemName() : NAME_None;
|
|
USkeletalMesh* SkeletalMesh = GetPreviewScene().IsValid() ? ToRawPtr(GetPreviewScene()->GetPreviewMeshComponent()->GetSkeletalMeshAsset()) : nullptr;
|
|
GetEditableSkeletonInternal()->HandlePasteSockets(DestBoneName, SkeletalMesh);
|
|
|
|
CreateFromSkeleton();
|
|
}
|
|
}
|
|
|
|
bool SSkeletonTree::CanPasteSockets() const
|
|
{
|
|
if(SkeletonTreeView->GetNumItemsSelected() == 1)
|
|
{
|
|
TArray<TSharedPtr<ISkeletonTreeItem>> SelectedItems = SkeletonTreeView->GetSelectedItems();
|
|
FSkeletonTreeSelection TreeSelection(SelectedItems);
|
|
|
|
return TreeSelection.IsSingleOfTypesSelected<FSkeletonTreeBoneItem, FSkeletonTreeVirtualBoneItem>();
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void SSkeletonTree::OnAddSocket()
|
|
{
|
|
// This adds a socket to the currently selected bone in the SKELETON, not the MESH.
|
|
TArray<TSharedPtr<ISkeletonTreeItem>> SelectedItems = SkeletonTreeView->GetSelectedItems();
|
|
FSkeletonTreeSelection TreeSelection(SelectedItems);
|
|
|
|
// Can only add a socket to one bone
|
|
if (TreeSelection.IsSingleOfTypesSelected<FSkeletonTreeBoneItem, FSkeletonTreeVirtualBoneItem>())
|
|
{
|
|
FName BoneName = TreeSelection.GetSingleSelectedItem()->GetRowItemName();
|
|
USkeletalMeshSocket* NewSocket = GetEditableSkeletonInternal()->HandleAddSocket(BoneName);
|
|
CreateFromSkeleton();
|
|
|
|
FSelectedSocketInfo SocketInfo(NewSocket, true);
|
|
SetSelectedSocket(SocketInfo);
|
|
|
|
// now let us choose the socket name
|
|
for (TSharedPtr<ISkeletonTreeItem>& Item : LinearItems)
|
|
{
|
|
if (Item->IsOfType<FSkeletonTreeSocketItem>())
|
|
{
|
|
if (Item->GetRowItemName() == NewSocket->SocketName)
|
|
{
|
|
OnRenameSelected();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void SSkeletonTree::OnCustomizeSocket()
|
|
{
|
|
// This should only be called on a skeleton socket, it copies the
|
|
// socket to the mesh so the user can edit it separately
|
|
TArray<TSharedPtr<ISkeletonTreeItem>> SelectedItems = SkeletonTreeView->GetSelectedItems();
|
|
FSkeletonTreeSelection TreeSelection(SelectedItems);
|
|
|
|
if(TreeSelection.IsSingleOfTypeSelected<FSkeletonTreeSocketItem>())
|
|
{
|
|
USkeletalMeshSocket* SocketToCustomize = StaticCastSharedPtr<FSkeletonTreeSocketItem>(TreeSelection.GetSingleSelectedItem())->GetSocket();
|
|
USkeletalMesh* SkeletalMesh = GetPreviewScene().IsValid() ? ToRawPtr(GetPreviewScene()->GetPreviewMeshComponent()->GetSkeletalMeshAsset()) : nullptr;
|
|
GetEditableSkeletonInternal()->HandleCustomizeSocket(SocketToCustomize, SkeletalMesh);
|
|
CreateFromSkeleton();
|
|
}
|
|
}
|
|
|
|
void SSkeletonTree::OnPromoteSocket()
|
|
{
|
|
// This should only be called on a mesh socket, it copies the
|
|
// socket to the skeleton so all meshes can use it
|
|
TArray<TSharedPtr<ISkeletonTreeItem>> SelectedItems = SkeletonTreeView->GetSelectedItems();
|
|
FSkeletonTreeSelection TreeSelection(SelectedItems);
|
|
|
|
// Can only customize one socket (CreateContextMenu() should prevent this firing!)
|
|
if(TreeSelection.IsSingleOfTypeSelected<FSkeletonTreeSocketItem>())
|
|
{
|
|
USkeletalMeshSocket* SocketToPromote = StaticCastSharedPtr<FSkeletonTreeSocketItem>(TreeSelection.GetSingleSelectedItem())->GetSocket();
|
|
GetEditableSkeletonInternal()->HandlePromoteSocket(SocketToPromote);
|
|
CreateFromSkeleton();
|
|
}
|
|
}
|
|
|
|
void SSkeletonTree::FillAttachAssetSubmenu(FMenuBuilder& MenuBuilder, const TSharedPtr<ISkeletonTreeItem> TargetItem)
|
|
{
|
|
FContentBrowserModule& ContentBrowserModule = FModuleManager::Get().LoadModuleChecked<FContentBrowserModule>(TEXT("ContentBrowser"));
|
|
|
|
TArray<UClass*> FilterClasses = FComponentAssetBrokerage::GetSupportedAssets(USceneComponent::StaticClass());
|
|
|
|
//Clean up the selection so it is relevant to Persona
|
|
FilterClasses.RemoveSingleSwap(UBlueprint::StaticClass(), EAllowShrinking::No); //Child actor components broker gives us blueprints which isn't wanted
|
|
FilterClasses.RemoveSingleSwap(USoundBase::StaticClass(), EAllowShrinking::No); //No sounds wanted
|
|
|
|
FAssetPickerConfig AssetPickerConfig;
|
|
AssetPickerConfig.Filter.bRecursiveClasses = true;
|
|
|
|
for(int i = 0; i < FilterClasses.Num(); ++i)
|
|
{
|
|
AssetPickerConfig.Filter.ClassPaths.Add(FilterClasses[i]->GetClassPathName());
|
|
}
|
|
|
|
|
|
AssetPickerConfig.OnAssetSelected = FOnAssetSelected::CreateSP(this, &SSkeletonTree::OnAssetSelectedFromPicker, TargetItem);
|
|
|
|
TSharedRef<SWidget> MenuContent = SNew(SBox)
|
|
.WidthOverride(384.f)
|
|
.HeightOverride(500.f)
|
|
[
|
|
ContentBrowserModule.Get().CreateAssetPicker(AssetPickerConfig)
|
|
];
|
|
MenuBuilder.AddWidget( MenuContent, FText::GetEmpty(), true);
|
|
}
|
|
|
|
void SSkeletonTree::OnAssetSelectedFromPicker(const FAssetData& AssetData, const TSharedPtr<ISkeletonTreeItem> TargetItem)
|
|
{
|
|
FSlateApplication::Get().DismissAllMenus();
|
|
TArray<FAssetData> Assets;
|
|
Assets.Add(AssetData);
|
|
|
|
AttachAssets(TargetItem.ToSharedRef(), Assets);
|
|
}
|
|
|
|
void SSkeletonTree::OnRemoveAllAssets()
|
|
{
|
|
GetEditableSkeletonInternal()->HandleRemoveAllAssets(GetPreviewScene());
|
|
|
|
CreateFromSkeleton();
|
|
}
|
|
|
|
bool SSkeletonTree::CanRemoveAllAssets() const
|
|
{
|
|
USkeletalMesh* SkeletalMesh = GetPreviewScene().IsValid() ? ToRawPtr(GetPreviewScene()->GetPreviewMeshComponent()->GetSkeletalMeshAsset()) : nullptr;
|
|
|
|
const bool bHasPreviewAttachedObjects = GetEditableSkeletonInternal()->GetSkeleton().PreviewAttachedAssetContainer.Num() > 0;
|
|
const bool bHasMeshPreviewAttachedObjects = ( SkeletalMesh && SkeletalMesh->GetPreviewAttachedAssetContainer().Num() );
|
|
|
|
return bHasPreviewAttachedObjects || bHasMeshPreviewAttachedObjects;
|
|
}
|
|
|
|
bool SSkeletonTree::CanRenameSelected() const
|
|
{
|
|
TArray<TSharedPtr<ISkeletonTreeItem>> SelectedItems = SkeletonTreeView->GetSelectedItems();
|
|
|
|
return SelectedItems.Num() == 1 && SelectedItems[0]->CanRenameItem();
|
|
}
|
|
|
|
void SSkeletonTree::OnRenameSelected()
|
|
{
|
|
TArray<TSharedPtr<ISkeletonTreeItem>> SelectedItems = SkeletonTreeView->GetSelectedItems();
|
|
|
|
if(SelectedItems.Num() == 1 && SelectedItems[0]->CanRenameItem())
|
|
{
|
|
SkeletonTreeView->RequestScrollIntoView(SelectedItems[0]);
|
|
DeferredRenameRequest = SelectedItems[0];
|
|
}
|
|
}
|
|
|
|
bool SSkeletonTree::OnIsSelectableOrNavigable(TSharedPtr<class ISkeletonTreeItem> InItem) const
|
|
{
|
|
return InItem && InItem->GetFilterResult() == ESkeletonTreeFilterResult::Shown;
|
|
}
|
|
|
|
void SSkeletonTree::OnSelectionChanged(TSharedPtr<ISkeletonTreeItem> Selection, ESelectInfo::Type SelectInfo)
|
|
{
|
|
TArray<TSharedPtr<ISkeletonTreeItem>> SelectedItems = SkeletonTreeView->GetSelectedItems();
|
|
|
|
if( Selection.IsValid() )
|
|
{
|
|
// Disable bone proxy ticking on all bone/virtual bones
|
|
for (TSharedPtr<ISkeletonTreeItem>& Item : LinearItems)
|
|
{
|
|
if (Item->IsOfType<FSkeletonTreeBoneItem>())
|
|
{
|
|
StaticCastSharedPtr<FSkeletonTreeBoneItem>(Item)->EnableBoneProxyTick(false);
|
|
}
|
|
else if (Item->IsOfType<FSkeletonTreeVirtualBoneItem>())
|
|
{
|
|
StaticCastSharedPtr<FSkeletonTreeVirtualBoneItem>(Item)->EnableBoneProxyTick(false);
|
|
}
|
|
}
|
|
|
|
//Get all the selected items
|
|
FSkeletonTreeSelection TreeSelection(SelectedItems);
|
|
|
|
if (GetPreviewScene().IsValid())
|
|
{
|
|
UDebugSkelMeshComponent* PreviewComponent = GetPreviewScene()->GetPreviewMeshComponent();
|
|
if (TreeSelection.SelectedItems.Num() > 0 && PreviewComponent)
|
|
{
|
|
// pick the first settable bone from the selection
|
|
for (TSharedPtr<ISkeletonTreeItem> Item : TreeSelection.SelectedItems)
|
|
{
|
|
if((Item->IsOfType<FSkeletonTreeBoneItem>() || Item->IsOfType<FSkeletonTreeVirtualBoneItem>()))
|
|
{
|
|
// enable ticking on the selected bone proxies
|
|
if (Item->IsOfType<FSkeletonTreeBoneItem>())
|
|
{
|
|
StaticCastSharedPtr<FSkeletonTreeBoneItem>(Item)->EnableBoneProxyTick(true);
|
|
}
|
|
else if (Item->IsOfType<FSkeletonTreeVirtualBoneItem>())
|
|
{
|
|
StaticCastSharedPtr<FSkeletonTreeVirtualBoneItem>(Item)->EnableBoneProxyTick(true);
|
|
}
|
|
|
|
// Test SelectInfo so we don't end up in an infinite loop due to delegates calling each other
|
|
if (SelectInfo != ESelectInfo::Direct)
|
|
{
|
|
FName BoneName = Item->GetRowItemName();
|
|
|
|
// Get bone index
|
|
int32 BoneIndex = PreviewComponent->GetBoneIndex(BoneName);
|
|
if (BoneIndex != INDEX_NONE)
|
|
{
|
|
GetPreviewScene()->SetSelectedBone(BoneName, SelectInfo);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
// Test SelectInfo so we don't end up in an infinite loop due to delegates calling each other
|
|
else if (SelectInfo != ESelectInfo::Direct && Item->IsOfType<FSkeletonTreeSocketItem>())
|
|
{
|
|
TSharedPtr<FSkeletonTreeSocketItem> SocketItem = StaticCastSharedPtr<FSkeletonTreeSocketItem>(Item);
|
|
USkeletalMeshSocket* Socket = SocketItem->GetSocket();
|
|
FSelectedSocketInfo SocketInfo(Socket, SocketItem->GetParentType() == ESocketParentType::Skeleton);
|
|
GetPreviewScene()->SetSelectedSocket(SocketInfo);
|
|
}
|
|
else if (Item->IsOfType<FSkeletonTreeAttachedAssetItem>())
|
|
{
|
|
GetPreviewScene()->DeselectAll();
|
|
}
|
|
}
|
|
PreviewComponent->PostInitMeshObject(PreviewComponent->MeshObject);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (GetPreviewScene().IsValid())
|
|
{
|
|
// Tell the preview scene if the user ctrl-clicked the selected bone/socket to de-select it
|
|
GetPreviewScene()->DeselectAll();
|
|
}
|
|
}
|
|
|
|
TArrayView<TSharedPtr<ISkeletonTreeItem>> ArrayView(SelectedItems);
|
|
OnSelectionChangedMulticast.Broadcast(ArrayView, SelectInfo);
|
|
}
|
|
|
|
void SSkeletonTree::AttachAssets(const TSharedRef<ISkeletonTreeItem>& TargetItem, const TArray<FAssetData>& AssetData)
|
|
{
|
|
bool bAllAssetWereLoaded = true;
|
|
TArray<UObject*> DroppedObjects;
|
|
for (int32 AssetIdx = 0; AssetIdx < AssetData.Num(); ++AssetIdx)
|
|
{
|
|
UObject* Object = AssetData[AssetIdx].GetAsset();
|
|
if ( Object != NULL )
|
|
{
|
|
if (FComponentAssetBrokerage::GetPrimaryComponentForAsset(Object->GetClass()) != nullptr)
|
|
{
|
|
DroppedObjects.Add(Object);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
bAllAssetWereLoaded = false;
|
|
}
|
|
}
|
|
|
|
if(bAllAssetWereLoaded)
|
|
{
|
|
FName AttachToName = TargetItem->GetAttachName();
|
|
bool bAttachToMesh = TargetItem->IsOfType<FSkeletonTreeSocketItem>() &&
|
|
StaticCastSharedRef<FSkeletonTreeSocketItem>(TargetItem)->GetParentType() == ESocketParentType::Mesh;
|
|
|
|
GetEditableSkeletonInternal()->HandleAttachAssets(DroppedObjects, AttachToName, bAttachToMesh, GetPreviewScene());
|
|
CreateFromSkeleton();
|
|
}
|
|
}
|
|
|
|
void SSkeletonTree::OnItemScrolledIntoView( TSharedPtr<ISkeletonTreeItem> InItem, const TSharedPtr<ITableRow>& InWidget)
|
|
{
|
|
if(DeferredRenameRequest.IsValid())
|
|
{
|
|
DeferredRenameRequest->RequestRename();
|
|
DeferredRenameRequest.Reset();
|
|
}
|
|
}
|
|
|
|
void SSkeletonTree::OnTreeDoubleClick( TSharedPtr<ISkeletonTreeItem> InItem )
|
|
{
|
|
InItem->OnItemDoubleClicked();
|
|
}
|
|
|
|
void SSkeletonTree::SetTreeItemExpansionRecursive(TSharedPtr< ISkeletonTreeItem > TreeItem, bool bInExpansionState) const
|
|
{
|
|
SkeletonTreeView->SetItemExpansion(TreeItem, bInExpansionState);
|
|
|
|
// Recursively go through the children.
|
|
for (auto It = TreeItem->GetChildren().CreateIterator(); It; ++It)
|
|
{
|
|
SetTreeItemExpansionRecursive(*It, bInExpansionState);
|
|
}
|
|
}
|
|
|
|
void SSkeletonTree::PostUndo(bool bSuccess)
|
|
{
|
|
// Rebuild the tree view whenever we undo a change to the skeleton
|
|
CreateFromSkeleton();
|
|
HandleTreeRefresh();
|
|
}
|
|
|
|
void SSkeletonTree::PostRedo(bool bSuccess)
|
|
{
|
|
// Rebuild the tree view whenever we redo a change to the skeleton
|
|
CreateFromSkeleton();
|
|
HandleTreeRefresh();
|
|
}
|
|
|
|
void SSkeletonTree::OnFilterTextChanged( const FText& SearchText )
|
|
{
|
|
FilterText = SearchText;
|
|
|
|
ApplyFilter();
|
|
}
|
|
|
|
void SSkeletonTree::HandlePackageReloaded(const EPackageReloadPhase InPackageReloadPhase, FPackageReloadedEvent* InPackageReloadedEvent)
|
|
{
|
|
if (InPackageReloadPhase == EPackageReloadPhase::PostPackageFixup)
|
|
{
|
|
for (const auto& RepointedObjectPair : InPackageReloadedEvent->GetRepointedObjects())
|
|
{
|
|
if (USkeleton* NewObject = Cast<USkeleton>(RepointedObjectPair.Value))
|
|
{
|
|
if (&GetEditableSkeletonInternal()->GetSkeleton() == NewObject)
|
|
{
|
|
Refresh();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
TSharedRef<SWidget> SSkeletonTree::GetBlendProfileColumnMenuContent()
|
|
{
|
|
FToolMenuContext MenuContext(UICommandList, Extenders);
|
|
USkeletonTreeMenuContext* SkeletonTreeMenuContext = NewObject<USkeletonTreeMenuContext>();
|
|
SkeletonTreeMenuContext->SkeletonTree = SharedThis(this);
|
|
MenuContext.AddObject(SkeletonTreeMenuContext);
|
|
|
|
return UToolMenus::Get()->GenerateWidget("SkeletonTree.BlendProfilesMenu", MenuContext);
|
|
}
|
|
|
|
void SSkeletonTree::ExpandTreeOnSelection(TSharedPtr<ISkeletonTreeItem> RowToExpand, bool bForce)
|
|
{
|
|
if(GetDefault<UPersonaOptions>()->bExpandTreeOnSelection || bForce)
|
|
{
|
|
RowToExpand = RowToExpand->GetParent();
|
|
while(RowToExpand.IsValid())
|
|
{
|
|
SkeletonTreeView->SetItemExpansion(RowToExpand, true);
|
|
RowToExpand = RowToExpand->GetParent();
|
|
}
|
|
}
|
|
}
|
|
|
|
void SSkeletonTree::RegisterBlendProfileMenu()
|
|
{
|
|
const FName MenuName("SkeletonTree.BlendProfilesMenu");
|
|
if (UToolMenus::Get()->IsMenuRegistered(MenuName))
|
|
{
|
|
return;
|
|
}
|
|
|
|
FToolMenuOwnerScoped OwnerScoped(this);
|
|
|
|
UToolMenu* Menu = UToolMenus::Get()->RegisterMenu(MenuName);
|
|
|
|
Menu->AddDynamicSection(NAME_None,
|
|
FNewSectionConstructChoice(FNewToolMenuDelegate::CreateLambda([](UToolMenu* InMenu)
|
|
{
|
|
CreateBlendProfileMenu(InMenu);
|
|
})));
|
|
}
|
|
|
|
void SSkeletonTree::CreateBlendProfileMenu(UToolMenu* InMenu)
|
|
{
|
|
USkeletonTreeMenuContext* MenuContext = InMenu->Context.FindContext<USkeletonTreeMenuContext>();
|
|
if(MenuContext == nullptr)
|
|
{
|
|
return;
|
|
}
|
|
|
|
TSharedPtr<SSkeletonTree> SkeletonTree = MenuContext->SkeletonTree.Pin();
|
|
if(!SkeletonTree.IsValid())
|
|
{
|
|
return;
|
|
}
|
|
|
|
const FSkeletonTreeCommands& Actions = FSkeletonTreeCommands::Get();
|
|
|
|
static const FName BlendProfileSectionNames[]
|
|
{
|
|
TEXT("BlendProfileTimeActions"),
|
|
TEXT("BlendProfileWeightActions"),
|
|
TEXT("BlendMaskActions")
|
|
};
|
|
|
|
InMenu->AddSection(BlendProfileSectionNames[0], LOCTEXT("BlendProfilesTime", "Blend Profiles - Time"));
|
|
InMenu->AddSection(BlendProfileSectionNames[1], LOCTEXT("BlendProfilesWeight", "Blend Profiles - Weight"));
|
|
InMenu->AddSection(BlendProfileSectionNames[2], LOCTEXT("BlendProfiles", "Blend Masks"));
|
|
FToolMenuSection* BlendProfileSections[] =
|
|
{
|
|
InMenu->FindSection(BlendProfileSectionNames[0]),
|
|
InMenu->FindSection(BlendProfileSectionNames[1]),
|
|
InMenu->FindSection(BlendProfileSectionNames[2])
|
|
};
|
|
|
|
static const FText SelectBlendProfileToolTipText = LOCTEXT("SelectBlendProfileTooltip", "Select this blend profile for editing.");
|
|
static const FText SelectBlendMaskToolTipText = LOCTEXT("SelectBlendMaskTooltip", "Select this blend mask for editing.");
|
|
static const FText SelectBlendProfileToolTipTexts[] =
|
|
{
|
|
SelectBlendProfileToolTipText,
|
|
SelectBlendProfileToolTipText,
|
|
SelectBlendMaskToolTipText
|
|
};
|
|
|
|
UEnum* ModeEnum = StaticEnum<EBlendProfileMode>();
|
|
check(ModeEnum);
|
|
|
|
for (UBlendProfile* Profile : SkeletonTree->GetEditableSkeletonInternal()->GetBlendProfiles())
|
|
{
|
|
if (Profile)
|
|
{
|
|
int32 EnumIndex = ModeEnum->GetIndexByValue((int64)Profile->GetMode());
|
|
BlendProfileSections[EnumIndex]->AddMenuEntry(
|
|
Profile->GetFName(),
|
|
FText::FromName(Profile->GetFName()),
|
|
SelectBlendProfileToolTipTexts[EnumIndex],
|
|
FSlateIcon(),
|
|
FToolUIActionChoice(
|
|
FUIAction(
|
|
FExecuteAction::CreateSP(SkeletonTree->BlendProfilePicker.ToSharedRef(), &SBlendProfilePicker::SetSelectedProfile, Profile, true),
|
|
FCanExecuteAction(),
|
|
FIsActionChecked::CreateSP(SkeletonTree.Get(), &SSkeletonTree::IsBlendProfileSelected, Profile->GetFName())
|
|
)
|
|
),
|
|
EUserInterfaceActionType::RadioButton
|
|
);
|
|
}
|
|
}
|
|
|
|
if (SkeletonTree->BlendProfilePicker->GetSelectedBlendProfileName() != NAME_None)
|
|
{
|
|
FToolMenuSection& EditSection = InMenu->AddSection(TEXT("BlendProfileEdit"), LOCTEXT("EditBlendProfilesSection", "Edit"));
|
|
|
|
EditSection.AddMenuEntry(
|
|
"ClearBlendProfile",
|
|
LOCTEXT("Clear", "Clear Selected"),
|
|
LOCTEXT("Clear_ToolTip", "Clear the selected blend profile/mask."),
|
|
FSlateIcon(),
|
|
FToolUIActionChoice(FUIAction(FExecuteAction::CreateSP(SkeletonTree->BlendProfilePicker.ToSharedRef(), &SBlendProfilePicker::OnClearSelection))));
|
|
|
|
EditSection.AddMenuEntry(
|
|
Actions.RenameBlendProfile,
|
|
FText::Format(LOCTEXT("RenameBlendProfileLabel", "Rename {0}"),
|
|
FText::FromName(SkeletonTree->BlendProfilePicker->GetSelectedBlendProfileName())));
|
|
|
|
EditSection.AddMenuEntry(
|
|
Actions.DeleteCurrentBlendProfile,
|
|
FText::Format(LOCTEXT("DeleteBlendProfileLabel", "Delete {0}"),
|
|
FText::FromName(SkeletonTree->BlendProfilePicker->GetSelectedBlendProfileName())));
|
|
}
|
|
|
|
{
|
|
FToolMenuSection& NewSection = InMenu->AddSection(TEXT("BlendProfileNew"), LOCTEXT("NewBlendProfiles", "New"));
|
|
NewSection.AddMenuEntry(Actions.CreateTimeBlendProfile);
|
|
NewSection.AddMenuEntry(Actions.CreateWeightBlendProfile);
|
|
NewSection.AddMenuEntry(Actions.CreateBlendMask);
|
|
}
|
|
}
|
|
|
|
void SSkeletonTree::OnCreateBlendProfile(const EBlendProfileMode InMode)
|
|
{
|
|
// Ensure the Blend Profile Column is Visible
|
|
BlendProfilePicker->OnClearSelection();
|
|
SkeletonTreeView->GetHeaderRow()->SetShowGeneratedColumn(ISkeletonTree::Columns::BlendProfile);
|
|
|
|
// Set our NewBlendProfileMode for our BlendProfileHeader to use when the text is commited.
|
|
NewBlendProfileMode = InMode;
|
|
|
|
// Activate the Header Entry Box
|
|
BlendProfileHeader->SetReadOnly(false);
|
|
BlendProfileHeader->EnterEditingMode();
|
|
bIsCreateNewBlendProfile = true;
|
|
}
|
|
|
|
void SSkeletonTree::OnDeleteCurrentBlendProfile()
|
|
{
|
|
GetEditableSkeletonInternal()->RemoveBlendProfile(BlendProfilePicker->GetSelectedBlendProfile());
|
|
BlendProfilePicker->OnClearSelection();
|
|
}
|
|
|
|
void SSkeletonTree::OnRenameBlendProfile()
|
|
{
|
|
// Activate the Header Entry Box
|
|
BlendProfileHeader->SetReadOnly(false);
|
|
BlendProfileHeader->EnterEditingMode();
|
|
}
|
|
|
|
bool SSkeletonTree::IsBlendProfileSelected(FName ProfileName) const
|
|
{
|
|
return BlendProfilePicker->GetSelectedBlendProfileName() == ProfileName;
|
|
}
|
|
|
|
void SSkeletonTree::Refresh()
|
|
{
|
|
CreateFromSkeleton();
|
|
}
|
|
|
|
void SSkeletonTree::RefreshFilter()
|
|
{
|
|
ApplyFilter();
|
|
}
|
|
|
|
void SSkeletonTree::SetSkeletalMesh(USkeletalMesh* NewSkeletalMesh)
|
|
{
|
|
if (GetPreviewScene().IsValid())
|
|
{
|
|
GetPreviewScene()->SetPreviewMesh(NewSkeletalMesh);
|
|
}
|
|
|
|
CreateFromSkeleton();
|
|
}
|
|
|
|
void SSkeletonTree::SetSelectedSocket( const FSelectedSocketInfo& SocketInfo )
|
|
{
|
|
if (!bSelecting)
|
|
{
|
|
TGuardValue<bool> RecursionGuard(bSelecting, true);
|
|
|
|
// Firstly, find which row (if any) contains the socket requested
|
|
for (auto SkeletonRowIt = LinearItems.CreateConstIterator(); SkeletonRowIt; ++SkeletonRowIt)
|
|
{
|
|
TSharedPtr<ISkeletonTreeItem> SkeletonRow = *(SkeletonRowIt);
|
|
|
|
if (SkeletonRow->GetFilterResult() != ESkeletonTreeFilterResult::Hidden && SkeletonRow->IsOfType<FSkeletonTreeSocketItem>() && StaticCastSharedPtr<FSkeletonTreeSocketItem>(SkeletonRow)->GetSocket() == SocketInfo.Socket)
|
|
{
|
|
SkeletonTreeView->SetItemSelection(SkeletonRow, true);
|
|
ExpandTreeOnSelection(SkeletonRow);
|
|
SkeletonTreeView->RequestScrollIntoView(SkeletonRow);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void SSkeletonTree::SetSelectedBone( const FName& BoneName, ESelectInfo::Type InSelectInfo )
|
|
{
|
|
if (!bSelecting)
|
|
{
|
|
TGuardValue<bool> RecursionGuard(bSelecting, true);
|
|
|
|
// Find which row (if any) contains the bone requested
|
|
for (auto SkeletonRowIt = LinearItems.CreateConstIterator(); SkeletonRowIt; ++SkeletonRowIt)
|
|
{
|
|
TSharedPtr<ISkeletonTreeItem> SkeletonRow = *(SkeletonRowIt);
|
|
|
|
if (SkeletonRow->GetFilterResult() != ESkeletonTreeFilterResult::Hidden && (SkeletonRow->IsOfType<FSkeletonTreeBoneItem>() || SkeletonRow->IsOfType<FSkeletonTreeVirtualBoneItem>()) && SkeletonRow->GetRowItemName() == BoneName)
|
|
{
|
|
SkeletonTreeView->SetItemSelection(SkeletonRow, true, InSelectInfo);
|
|
ExpandTreeOnSelection(SkeletonRow);
|
|
SkeletonTreeView->RequestScrollIntoView(SkeletonRow);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void SSkeletonTree::SetSelectedBones(const TArray<FName>& InBoneNames, ESelectInfo::Type InSelectInfo)
|
|
{
|
|
if (InBoneNames.IsEmpty())
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!bSelecting)
|
|
{
|
|
TGuardValue<bool> RecursionGuard(bSelecting, true);
|
|
|
|
const TArray<TSharedPtr<ISkeletonTreeItem>> SkeletonTreeItems = LinearItems.FilterByPredicate([&InBoneNames](const TSharedPtr<ISkeletonTreeItem>& SkeletonRow)
|
|
{
|
|
if (SkeletonRow->GetFilterResult() == ESkeletonTreeFilterResult::Hidden)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (SkeletonRow->IsOfType<FSkeletonTreeBoneItem>() || SkeletonRow->IsOfType<FSkeletonTreeVirtualBoneItem>())
|
|
{
|
|
return InBoneNames.Contains(SkeletonRow->GetRowItemName());
|
|
}
|
|
|
|
return false;
|
|
});
|
|
|
|
if (SkeletonTreeItems.IsEmpty())
|
|
{
|
|
return;
|
|
}
|
|
|
|
ExpandTreeOnSelection(SkeletonTreeItems[0]);
|
|
constexpr bool bSelected = true;
|
|
SkeletonTreeView->SetItemSelection(SkeletonTreeItems, bSelected, InSelectInfo);
|
|
SkeletonTreeView->RequestScrollIntoView(SkeletonTreeItems[0]);
|
|
}
|
|
}
|
|
|
|
void SSkeletonTree::DeselectAll()
|
|
{
|
|
if (!bSelecting)
|
|
{
|
|
TGuardValue<bool> RecursionGuard(bSelecting, true);
|
|
SkeletonTreeView->ClearSelection();
|
|
}
|
|
}
|
|
|
|
void SSkeletonTree::NotifyUser( FNotificationInfo& NotificationInfo )
|
|
{
|
|
TSharedPtr<SNotificationItem> Notification = FSlateNotificationManager::Get().AddNotification( NotificationInfo );
|
|
if ( Notification.IsValid() )
|
|
{
|
|
Notification->SetCompletionState( SNotificationItem::CS_Fail );
|
|
}
|
|
}
|
|
|
|
void SSkeletonTree::RegisterNewMenu()
|
|
{
|
|
const FName MenuName("SkeletonTree.NewMenu");
|
|
if (UToolMenus::Get()->IsMenuRegistered(MenuName))
|
|
{
|
|
return;
|
|
}
|
|
|
|
FToolMenuOwnerScoped OwnerScoped(this);
|
|
|
|
const FSkeletonTreeCommands& Actions = FSkeletonTreeCommands::Get();
|
|
|
|
UToolMenu* Menu = UToolMenus::Get()->RegisterMenu(MenuName);
|
|
|
|
{
|
|
FToolMenuSection& CreateSection = Menu->AddSection("CreateNew", LOCTEXT("SkeletonCreateNew", "Create"));
|
|
|
|
CreateSection.AddMenuEntry(Actions.AddSocket);
|
|
CreateSection.AddSubMenu(
|
|
"VirtualBones",
|
|
LOCTEXT("AddVirtualBone", "Add Virtual Bone"),
|
|
LOCTEXT("AddVirtualBone_ToolTip", "Adds a virtual bone to the skeleton."),
|
|
FNewToolMenuDelegate::CreateLambda([](UToolMenu* InMenu)
|
|
{
|
|
USkeletonTreeMenuContext* MenuContext = InMenu->Context.FindContext<USkeletonTreeMenuContext>();
|
|
if(MenuContext == nullptr)
|
|
{
|
|
return;
|
|
}
|
|
|
|
TSharedPtr<SSkeletonTree> SkeletonTree = MenuContext->SkeletonTree.Pin();
|
|
if(!SkeletonTree.IsValid())
|
|
{
|
|
return;
|
|
}
|
|
|
|
TSharedRef<SBoneTreeMenu> MenuContent = SSkeletonTree::CreateVirtualBoneMenu(SkeletonTree.Get());
|
|
FToolMenuEntry WidgetEntry = FToolMenuEntry::InitWidget("VirtualBones", MenuContent, FText());
|
|
InMenu->AddMenuEntry(NAME_None, WidgetEntry);
|
|
}));
|
|
}
|
|
|
|
{
|
|
FToolMenuSection& BlendSection = Menu->AddSection("Blend", LOCTEXT("SkeletonBlend", "Blend"));
|
|
BlendSection.AddMenuEntry(Actions.CreateTimeBlendProfile);
|
|
BlendSection.AddMenuEntry(Actions.CreateWeightBlendProfile);
|
|
BlendSection.AddMenuEntry(Actions.CreateBlendMask);
|
|
}
|
|
}
|
|
|
|
TSharedRef< SWidget > SSkeletonTree::CreateNewMenuWidget()
|
|
{
|
|
FToolMenuContext MenuContext(UICommandList, Extenders);
|
|
USkeletonTreeMenuContext* SkeletonTreeMenuContext = NewObject<USkeletonTreeMenuContext>();
|
|
SkeletonTreeMenuContext->SkeletonTree = SharedThis(this);
|
|
MenuContext.AddObject(SkeletonTreeMenuContext);
|
|
|
|
return UToolMenus::Get()->GenerateWidget("SkeletonTree.NewMenu", MenuContext);
|
|
}
|
|
|
|
void SSkeletonTree::RegisterFilterMenu()
|
|
{
|
|
const FName MenuName("SkeletonTree.FilterMenu");
|
|
if (UToolMenus::Get()->IsMenuRegistered(MenuName))
|
|
{
|
|
return;
|
|
}
|
|
|
|
FToolMenuOwnerScoped OwnerScoped(this);
|
|
|
|
const FSkeletonTreeCommands& Actions = FSkeletonTreeCommands::Get();
|
|
|
|
UToolMenu* Menu = UToolMenus::Get()->RegisterMenu(MenuName);
|
|
|
|
{
|
|
FToolMenuSection& BlendProfilesSection = Menu->AddSection("BlendProfiles", LOCTEXT("BlendProfilesMenuHeading", "Blend Profiles"));
|
|
BlendProfilesSection.AddSubMenu(
|
|
"BlendProfiles",
|
|
LOCTEXT("BlendProfilesSubMenu", "Blend Profiles"),
|
|
LOCTEXT("BlendProfilesSubMenuTooltip", "Edit Blend Profiles in this Skeleton"),
|
|
FNewToolMenuChoice(FNewToolMenuDelegate::CreateStatic(&SSkeletonTree::CreateBlendProfileMenu)));
|
|
}
|
|
|
|
{
|
|
FToolMenuSection& OptionsSection = Menu->AddSection("FilterOptions", LOCTEXT("OptionsMenuHeading", "Options"));
|
|
OptionsSection.AddMenuEntry(Actions.ShowRetargeting);
|
|
OptionsSection.AddMenuEntry(Actions.FilteringFlattensHierarchy);
|
|
OptionsSection.AddMenuEntry(Actions.HideParentsWhenFiltering);
|
|
OptionsSection.AddMenuEntry(Actions.ShowDebugVisualization);
|
|
OptionsSection.AddMenuEntry(Actions.ShowBoneIndex);
|
|
}
|
|
|
|
{
|
|
FToolMenuSection& BonesSection = Menu->AddSection("FilterBones", LOCTEXT("BonesMenuHeading", "Bones"));
|
|
BonesSection.AddMenuEntry(Actions.ShowAllBones);
|
|
BonesSection.AddMenuEntry(Actions.ShowMeshBones);
|
|
BonesSection.AddMenuEntry(Actions.ShowLODBones);
|
|
BonesSection.AddMenuEntry(Actions.ShowWeightedBones);
|
|
BonesSection.AddMenuEntry(Actions.HideBones);
|
|
}
|
|
|
|
{
|
|
FToolMenuSection& BonesSection = Menu->AddSection("FilterSockets", LOCTEXT("SocketsMenuHeading", "Sockets"));
|
|
BonesSection.AddMenuEntry(Actions.ShowActiveSockets);
|
|
BonesSection.AddMenuEntry(Actions.ShowMeshSockets);
|
|
BonesSection.AddMenuEntry(Actions.ShowSkeletonSockets);
|
|
BonesSection.AddMenuEntry(Actions.ShowAllSockets);
|
|
BonesSection.AddMenuEntry(Actions.HideSockets);
|
|
}
|
|
}
|
|
|
|
TSharedRef< SWidget > SSkeletonTree::CreateFilterMenuWidget()
|
|
{
|
|
FToolMenuContext MenuContext(UICommandList, Extenders);
|
|
USkeletonTreeMenuContext* SkeletonTreeMenuContext = NewObject<USkeletonTreeMenuContext>();
|
|
SkeletonTreeMenuContext->SkeletonTree = SharedThis(this);
|
|
MenuContext.AddObject(SkeletonTreeMenuContext);
|
|
|
|
return UToolMenus::Get()->GenerateWidget("SkeletonTree.FilterMenu", MenuContext);
|
|
}
|
|
|
|
void SSkeletonTree::SetBoneFilter( EBoneFilter InBoneFilter )
|
|
{
|
|
check( InBoneFilter < EBoneFilter::Count );
|
|
BoneFilter = InBoneFilter;
|
|
|
|
ApplyFilter();
|
|
}
|
|
|
|
bool SSkeletonTree::IsBoneFilter( EBoneFilter InBoneFilter ) const
|
|
{
|
|
return BoneFilter == InBoneFilter;
|
|
}
|
|
|
|
void SSkeletonTree::SetSocketFilter( ESocketFilter InSocketFilter )
|
|
{
|
|
check( InSocketFilter < ESocketFilter::Count );
|
|
SocketFilter = InSocketFilter;
|
|
|
|
SetPreviewComponentSocketFilter();
|
|
|
|
ApplyFilter();
|
|
}
|
|
|
|
void SSkeletonTree::SetPreviewComponentSocketFilter() const
|
|
{
|
|
// Set the socket filter in the debug skeletal mesh component so the viewport can share the filter settings
|
|
if (GetPreviewScene().IsValid())
|
|
{
|
|
UDebugSkelMeshComponent* PreviewComponent = GetPreviewScene()->GetPreviewMeshComponent();
|
|
|
|
bool bAllOrActive = (SocketFilter == ESocketFilter::All || SocketFilter == ESocketFilter::Active);
|
|
|
|
if (PreviewComponent)
|
|
{
|
|
PreviewComponent->bMeshSocketsVisible = bAllOrActive || SocketFilter == ESocketFilter::Mesh;
|
|
PreviewComponent->bSkeletonSocketsVisible = bAllOrActive || SocketFilter == ESocketFilter::Skeleton;
|
|
}
|
|
}
|
|
}
|
|
|
|
bool SSkeletonTree::IsSocketFilter( ESocketFilter InSocketFilter ) const
|
|
{
|
|
return SocketFilter == InSocketFilter;
|
|
}
|
|
|
|
FText SSkeletonTree::GetFilterMenuTooltip() const
|
|
{
|
|
TArray<FText> FilterLabels;
|
|
|
|
if(Builder->IsShowingBones())
|
|
{
|
|
switch ( BoneFilter )
|
|
{
|
|
case EBoneFilter::All:
|
|
FilterLabels.Add(LOCTEXT( "BoneFilterMenuAll", "Bones" ));
|
|
break;
|
|
|
|
case EBoneFilter::Mesh:
|
|
FilterLabels.Add(LOCTEXT( "BoneFilterMenuMesh", "Mesh Bones" ));
|
|
break;
|
|
|
|
case EBoneFilter::LOD:
|
|
FilterLabels.Add(LOCTEXT("BoneFilterMenuLOD", "LOD Bones"));
|
|
break;
|
|
|
|
case EBoneFilter::Weighted:
|
|
FilterLabels.Add(LOCTEXT( "BoneFilterMenuWeighted", "Weighted Bones" ));
|
|
break;
|
|
|
|
case EBoneFilter::None:
|
|
break;
|
|
|
|
default:
|
|
// Unknown mode
|
|
check(false);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if(Builder->IsShowingSockets())
|
|
{
|
|
switch (SocketFilter)
|
|
{
|
|
case ESocketFilter::Active:
|
|
FilterLabels.Add(LOCTEXT("SocketFilterMenuActive", "Active Sockets"));
|
|
break;
|
|
|
|
case ESocketFilter::Mesh:
|
|
FilterLabels.Add(LOCTEXT("SocketFilterMenuMesh", "Mesh Sockets"));
|
|
break;
|
|
|
|
case ESocketFilter::Skeleton:
|
|
FilterLabels.Add(LOCTEXT("SocketFilterMenuSkeleton", "Skeleton Sockets"));
|
|
break;
|
|
|
|
case ESocketFilter::All:
|
|
FilterLabels.Add(LOCTEXT("SocketFilterMenuAll", "All Sockets"));
|
|
break;
|
|
|
|
case ESocketFilter::None:
|
|
break;
|
|
|
|
default:
|
|
// Unknown mode
|
|
check(false);
|
|
break;
|
|
}
|
|
}
|
|
|
|
OnGetFilterText.ExecuteIfBound(FilterLabels);
|
|
|
|
FText Label;
|
|
if(FilterLabels.Num() > 0)
|
|
{
|
|
Label = FText::Format(LOCTEXT("FilterMenuLabelFormatStart", "Showing: {0}"), FilterLabels[0]);
|
|
for(int32 LabelIndex = 1; LabelIndex < FilterLabels.Num(); ++LabelIndex)
|
|
{
|
|
Label = FText::Format(LOCTEXT("FilterMenuLabelFormat", "{0}, {1}"), Label, FilterLabels[LabelIndex]);
|
|
}
|
|
Label = FText::Format(LOCTEXT("FilterMenuLabelFormatEnd", "{0}\nShift-clicking on items will 'pin' them to the skeleton tree."), Label);
|
|
}
|
|
else
|
|
{
|
|
Label = LOCTEXT("ShowingNoneLabel", "Filters.\nShift-clicking on items will 'pin' them to the skeleton tree.");
|
|
}
|
|
|
|
return Label;
|
|
}
|
|
|
|
bool SSkeletonTree::IsAddingSocketsAllowed() const
|
|
{
|
|
if ( SocketFilter == ESocketFilter::Skeleton ||
|
|
SocketFilter == ESocketFilter::Active ||
|
|
SocketFilter == ESocketFilter::All )
|
|
{
|
|
TArray<TSharedPtr<ISkeletonTreeItem>> SelectedItems = SkeletonTreeView->GetSelectedItems();
|
|
FSkeletonTreeSelection TreeSelection(SelectedItems);
|
|
return TreeSelection.IsSingleOfTypesSelected<FSkeletonTreeBoneItem, FSkeletonTreeVirtualBoneItem>();
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
FReply SSkeletonTree::OnKeyDown( const FGeometry& MyGeometry, const FKeyEvent& InKeyEvent )
|
|
{
|
|
if ( UICommandList->ProcessCommandBindings( InKeyEvent ) )
|
|
{
|
|
return FReply::Handled();
|
|
}
|
|
|
|
return FReply::Unhandled();
|
|
}
|
|
|
|
bool SSkeletonTree::CanDeleteSelectedRows() const
|
|
{
|
|
TArray<TSharedPtr<ISkeletonTreeItem>> SelectedItems = SkeletonTreeView->GetSelectedItems();
|
|
FSkeletonTreeSelection TreeSelection(SelectedItems);
|
|
return (TreeSelection.HasSelectedOfType<FSkeletonTreeAttachedAssetItem>() || TreeSelection.HasSelectedOfType<FSkeletonTreeSocketItem>() || TreeSelection.HasSelectedOfType<FSkeletonTreeVirtualBoneItem>());
|
|
}
|
|
|
|
void SSkeletonTree::OnDeleteSelectedRows()
|
|
{
|
|
TArray<TSharedPtr<ISkeletonTreeItem>> SelectedItems = SkeletonTreeView->GetSelectedItems();
|
|
FSkeletonTreeSelection TreeSelection(SelectedItems);
|
|
|
|
if(TreeSelection.HasSelectedOfType<FSkeletonTreeAttachedAssetItem>() || TreeSelection.HasSelectedOfType<FSkeletonTreeSocketItem>() || TreeSelection.HasSelectedOfType<FSkeletonTreeVirtualBoneItem>())
|
|
{
|
|
FScopedTransaction Transaction( LOCTEXT( "SkeletonTreeDeleteSelected", "Delete selected sockets/meshes/bones from skeleton tree" ) );
|
|
|
|
DeleteAttachedAssets( TreeSelection.GetSelectedItems<FSkeletonTreeAttachedAssetItem>() );
|
|
DeleteSockets( TreeSelection.GetSelectedItems<FSkeletonTreeSocketItem>() );
|
|
DeleteVirtualBones( TreeSelection.GetSelectedItems<FSkeletonTreeVirtualBoneItem>() );
|
|
|
|
CreateFromSkeleton();
|
|
}
|
|
}
|
|
|
|
void SSkeletonTree::DeleteAttachedAssets(const TArray<TSharedPtr<FSkeletonTreeAttachedAssetItem>>& InDisplayedAttachedAssetInfos)
|
|
{
|
|
DeselectAll();
|
|
|
|
TArray<FPreviewAttachedObjectPair> AttachedObjects;
|
|
for(const TSharedPtr<FSkeletonTreeAttachedAssetItem>& AttachedAssetInfo : InDisplayedAttachedAssetInfos)
|
|
{
|
|
FPreviewAttachedObjectPair Pair;
|
|
Pair.SetAttachedObject(AttachedAssetInfo->GetAsset());
|
|
Pair.AttachedTo = AttachedAssetInfo->GetParentName();
|
|
|
|
AttachedObjects.Add(Pair);
|
|
}
|
|
|
|
GetEditableSkeletonInternal()->HandleDeleteAttachedAssets(AttachedObjects, GetPreviewScene());
|
|
}
|
|
|
|
void SSkeletonTree::DeleteSockets(const TArray<TSharedPtr<FSkeletonTreeSocketItem>>& InDisplayedSocketInfos)
|
|
{
|
|
DeselectAll();
|
|
|
|
TArray<FSelectedSocketInfo> SocketInfo;
|
|
|
|
for (const TSharedPtr<FSkeletonTreeSocketItem>& DisplayedSocketInfo : InDisplayedSocketInfos)
|
|
{
|
|
USkeletalMeshSocket* SocketToDelete = DisplayedSocketInfo->GetSocket();
|
|
SocketInfo.Add(FSelectedSocketInfo(SocketToDelete, DisplayedSocketInfo->GetParentType() == ESocketParentType::Skeleton));
|
|
}
|
|
|
|
GetEditableSkeletonInternal()->HandleDeleteSockets(SocketInfo, GetPreviewScene());
|
|
}
|
|
|
|
void SSkeletonTree::DeleteVirtualBones(const TArray<TSharedPtr<FSkeletonTreeVirtualBoneItem>>& InDisplayedVirtualBoneInfos)
|
|
{
|
|
DeselectAll();
|
|
|
|
TArray<FName> VirtualBoneInfo;
|
|
|
|
for (const TSharedPtr<FSkeletonTreeVirtualBoneItem>& DisplayedVirtualBoneInfo : InDisplayedVirtualBoneInfos)
|
|
{
|
|
VirtualBoneInfo.Add(DisplayedVirtualBoneInfo->GetRowItemName());
|
|
}
|
|
|
|
GetEditableSkeletonInternal()->HandleDeleteVirtualBones(VirtualBoneInfo, GetPreviewScene());
|
|
}
|
|
|
|
void SSkeletonTree::OnChangeShowingAdvancedOptions()
|
|
{
|
|
SkeletonTreeView->GetHeaderRow()->SetShowGeneratedColumn(ISkeletonTree::Columns::Retargeting, !IsShowingAdvancedOptions());
|
|
HandleTreeRefresh();
|
|
}
|
|
|
|
bool SSkeletonTree::IsShowingAdvancedOptions() const
|
|
{
|
|
return SkeletonTreeView->GetHeaderRow()->IsColumnVisible(ISkeletonTree::Columns::Retargeting);
|
|
}
|
|
|
|
void SSkeletonTree::OnChangeShowingDebugVisualizationOptions()
|
|
{
|
|
SkeletonTreeView->GetHeaderRow()->SetShowGeneratedColumn(ISkeletonTree::Columns::DebugVisualization, !IsShowingDebugVisualizationOptions());
|
|
}
|
|
|
|
bool SSkeletonTree::IsShowingDebugVisualizationOptions() const
|
|
{
|
|
return SkeletonTreeView->GetHeaderRow()->IsColumnVisible(ISkeletonTree::Columns::DebugVisualization);
|
|
}
|
|
|
|
UBlendProfile* SSkeletonTree::GetSelectedBlendProfile()
|
|
{
|
|
return BlendProfilePicker->GetSelectedBlendProfile();
|
|
}
|
|
|
|
FName SSkeletonTree::GetSelectedBlendProfileName() const
|
|
{
|
|
return BlendProfilePicker->GetSelectedBlendProfileName();
|
|
}
|
|
|
|
void SSkeletonTree::OnBlendProfileSelected(UBlendProfile* NewProfile)
|
|
{
|
|
SkeletonTreeView->GetHeaderRow()->RefreshColumns();
|
|
if (NewProfile != nullptr)
|
|
SkeletonTreeView->GetHeaderRow()->SetShowGeneratedColumn(ISkeletonTree::Columns::BlendProfile);
|
|
HandleTreeRefresh();
|
|
|
|
// When a new blend profile is created/selected - enable edition if name != None.
|
|
BlendProfileHeader->SetReadOnly(BlendProfilePicker->GetSelectedBlendProfileName() == NAME_None);
|
|
}
|
|
|
|
void SSkeletonTree::RecursiveSetBlendProfileScales(float InScaleToSet)
|
|
{
|
|
UBlendProfile* SelectedBlendProfile = BlendProfilePicker->GetSelectedBlendProfile();
|
|
if(SelectedBlendProfile)
|
|
{
|
|
TArray<FName> BoneNames;
|
|
TArray<TSharedPtr<ISkeletonTreeItem>> SelectedItems = SkeletonTreeView->GetSelectedItems();
|
|
FSkeletonTreeSelection TreeSelection(SelectedItems);
|
|
|
|
for(TSharedPtr<FSkeletonTreeBoneItem>& SelectedBone : TreeSelection.GetSelectedItems<FSkeletonTreeBoneItem>())
|
|
{
|
|
BoneNames.Add(SelectedBone->GetRowItemName());
|
|
}
|
|
|
|
GetEditableSkeletonInternal()->RecursiveSetBlendProfileScales(SelectedBlendProfile->GetFName(), BoneNames, InScaleToSet);
|
|
|
|
HandleTreeRefresh();
|
|
}
|
|
}
|
|
|
|
void SSkeletonTree::HandleTreeRefresh()
|
|
{
|
|
SkeletonTreeView->RequestTreeRefresh();
|
|
}
|
|
|
|
void SSkeletonTree::PostRenameSocket(UObject* InAttachedObject, const FName& InOldName, const FName& InNewName)
|
|
{
|
|
TSharedPtr<IPersonaPreviewScene> LinkedPreviewScene = GetPreviewScene();
|
|
if (LinkedPreviewScene.IsValid())
|
|
{
|
|
LinkedPreviewScene->RemoveAttachedObjectFromPreviewComponent(InAttachedObject, InOldName);
|
|
LinkedPreviewScene->AttachObjectToPreviewComponent(InAttachedObject, InNewName);
|
|
}
|
|
}
|
|
|
|
void SSkeletonTree::PostDuplicateSocket(UObject* InAttachedObject, const FName& InSocketName)
|
|
{
|
|
TSharedPtr<IPersonaPreviewScene> LinkedPreviewScene = GetPreviewScene();
|
|
if (LinkedPreviewScene.IsValid())
|
|
{
|
|
LinkedPreviewScene->AttachObjectToPreviewComponent(InAttachedObject, InSocketName);
|
|
}
|
|
|
|
CreateFromSkeleton();
|
|
}
|
|
|
|
void SSkeletonTree::PostSetSocketParent()
|
|
{
|
|
CreateFromSkeleton();
|
|
}
|
|
|
|
void RecursiveSetLODChange(UDebugSkelMeshComponent* PreviewComponent, TSharedPtr<ISkeletonTreeItem> TreeRow)
|
|
{
|
|
if (TreeRow->IsOfType<FSkeletonTreeBoneItem>())
|
|
{
|
|
StaticCastSharedPtr<FSkeletonTreeBoneItem>(TreeRow)->CacheLODChange(PreviewComponent);
|
|
}
|
|
|
|
for (auto& Child : TreeRow->GetChildren())
|
|
{
|
|
RecursiveSetLODChange(PreviewComponent, Child);
|
|
}
|
|
}
|
|
|
|
void SSkeletonTree::OnLODSwitched()
|
|
{
|
|
if (GetPreviewScene().IsValid())
|
|
{
|
|
UDebugSkelMeshComponent* PreviewComponent = GetPreviewScene()->GetPreviewMeshComponent();
|
|
|
|
if (PreviewComponent)
|
|
{
|
|
LastCachedLODForPreviewMeshComponent = PreviewComponent->GetPredictedLODLevel();
|
|
|
|
if (BoneFilter == EBoneFilter::Weighted || BoneFilter == EBoneFilter::LOD)
|
|
{
|
|
CreateFromSkeleton();
|
|
}
|
|
else
|
|
{
|
|
for (TSharedPtr<ISkeletonTreeItem>& Item : Items)
|
|
{
|
|
RecursiveSetLODChange(PreviewComponent, Item);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void SSkeletonTree::OnPreviewMeshChanged(USkeletalMesh* InOldSkeletalMesh, USkeletalMesh* InNewSkeletalMesh)
|
|
{
|
|
if (InOldSkeletalMesh != InNewSkeletalMesh || InNewSkeletalMesh == nullptr)
|
|
{
|
|
DeselectAll();
|
|
}
|
|
}
|
|
|
|
void SSkeletonTree::SelectItemsBy(TFunctionRef<bool(const TSharedRef<ISkeletonTreeItem>&, bool&)> Predicate, const ESelectInfo::Type SelectInfo) const
|
|
{
|
|
TArray<TPair<TSharedPtr<ISkeletonTreeItem>, bool>> ItemsToSelect;
|
|
TSharedPtr<ISkeletonTreeItem> ScrollItem = nullptr;
|
|
for (const TSharedPtr<ISkeletonTreeItem>& Item : LinearItems)
|
|
{
|
|
if(Item->GetFilterResult() != ESkeletonTreeFilterResult::Hidden)
|
|
{
|
|
bool bExpand = false;
|
|
if (Predicate(Item.ToSharedRef(), bExpand))
|
|
{
|
|
ItemsToSelect.Emplace(Item, bExpand);
|
|
ScrollItem = Item;
|
|
}
|
|
}
|
|
}
|
|
|
|
if(ItemsToSelect.Num() > 0)
|
|
{
|
|
SkeletonTreeView->ClearSelection();
|
|
|
|
for (const TPair<TSharedPtr<ISkeletonTreeItem>, bool>& ItemPair : ItemsToSelect)
|
|
{
|
|
TSharedPtr<ISkeletonTreeItem> Item = ItemPair.Key;
|
|
|
|
SkeletonTreeView->SetItemSelection(Item, true, SelectInfo);
|
|
if (ItemPair.Value)
|
|
{
|
|
if(Item->GetChildren().Num() == 0)
|
|
{
|
|
// leaf nodes expand their parent
|
|
TSharedPtr<ISkeletonTreeItem> ParentItem = Item->GetParent();
|
|
if(ParentItem.IsValid())
|
|
{
|
|
SkeletonTreeView->SetItemExpansion(ParentItem, true);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
SkeletonTreeView->SetItemExpansion(Item, true);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (ScrollItem.IsValid())
|
|
{
|
|
SkeletonTreeView->RequestScrollIntoView(ScrollItem);
|
|
}
|
|
}
|
|
|
|
void SSkeletonTree::DuplicateAndSelectSocket(const FSelectedSocketInfo& SocketInfoToDuplicate, const FName& NewParentBoneName /*= FName()*/)
|
|
{
|
|
USkeletalMesh* SkeletalMesh = GetPreviewScene().IsValid() ? ToRawPtr(GetPreviewScene()->GetPreviewMeshComponent()->GetSkeletalMeshAsset()) : nullptr;
|
|
USkeletalMeshSocket* NewSocket = GetEditableSkeleton()->DuplicateSocket(SocketInfoToDuplicate, NewParentBoneName, SkeletalMesh);
|
|
|
|
if (GetPreviewScene().IsValid())
|
|
{
|
|
GetPreviewScene()->SetSelectedSocket(FSelectedSocketInfo(NewSocket, SocketInfoToDuplicate.bSocketIsOnSkeleton));
|
|
}
|
|
|
|
CreateFromSkeleton();
|
|
|
|
FSelectedSocketInfo NewSocketInfo(NewSocket, SocketInfoToDuplicate.bSocketIsOnSkeleton);
|
|
SetSelectedSocket(NewSocketInfo);
|
|
}
|
|
|
|
void SSkeletonTree::ResetBoneTransforms(const bool bSelectedOnly)
|
|
{
|
|
OnResetBoneTransforms(bSelectedOnly);
|
|
}
|
|
|
|
void SSkeletonTree::HandleFocusCamera()
|
|
{
|
|
if (GetPreviewScene().IsValid())
|
|
{
|
|
GetPreviewScene()->FocusViews();
|
|
}
|
|
|
|
if(!SkeletonTreeView->GetSelectedItems().IsEmpty())
|
|
{
|
|
TSharedPtr<class ISkeletonTreeItem> SelectedRow = SkeletonTreeView->GetSelectedItems()[0];
|
|
ExpandTreeOnSelection(SelectedRow);
|
|
SkeletonTreeView->RequestScrollIntoView(SelectedRow);
|
|
}
|
|
}
|
|
|
|
ESkeletonTreeFilterResult SSkeletonTree::HandleFilterSkeletonTreeItem(const FSkeletonTreeFilterArgs& InArgs, const TSharedPtr<class ISkeletonTreeItem>& InItem)
|
|
{
|
|
ESkeletonTreeFilterResult Result = ESkeletonTreeFilterResult::Shown;
|
|
|
|
if(InItem->IsOfType<FSkeletonTreeBoneItem>() || InItem->IsOfType<FSkeletonTreeSocketItem>() || InItem->IsOfType<FSkeletonTreeAttachedAssetItem>() || InItem->IsOfType<FSkeletonTreeVirtualBoneItem>())
|
|
{
|
|
|
|
if (InArgs.TextFilter.IsValid())
|
|
{
|
|
if (InArgs.TextFilter->TestTextFilter(FBasicStringFilterExpressionContext(InItem->GetRowItemName().ToString())))
|
|
{
|
|
Result = ESkeletonTreeFilterResult::ShownHighlighted;
|
|
}
|
|
else
|
|
{
|
|
Result = ESkeletonTreeFilterResult::Hidden;
|
|
}
|
|
}
|
|
|
|
|
|
if (InItem->IsOfType<FSkeletonTreeBoneItem>())
|
|
{
|
|
TSharedPtr<FSkeletonTreeBoneItem> BoneItem = StaticCastSharedPtr<FSkeletonTreeBoneItem>(InItem);
|
|
|
|
if (BoneFilter == EBoneFilter::None)
|
|
{
|
|
Result = ESkeletonTreeFilterResult::Hidden;
|
|
}
|
|
else if (GetPreviewScene().IsValid())
|
|
{
|
|
UDebugSkelMeshComponent* PreviewMeshComponent = GetPreviewScene()->GetPreviewMeshComponent();
|
|
if (PreviewMeshComponent)
|
|
{
|
|
const int32 BoneMeshIndex = PreviewMeshComponent->GetBoneIndex(BoneItem->GetRowItemName());
|
|
|
|
// Remove non-mesh bones if we're filtering
|
|
if ((BoneFilter == EBoneFilter::Mesh || BoneFilter == EBoneFilter::Weighted || BoneFilter == EBoneFilter::LOD) &&
|
|
BoneMeshIndex == INDEX_NONE)
|
|
{
|
|
Result = ESkeletonTreeFilterResult::Hidden;
|
|
}
|
|
|
|
// Remove non-vertex-weighted bones if we're filtering
|
|
else if (BoneFilter == EBoneFilter::Weighted && !BoneItem->IsBoneWeighted(BoneMeshIndex, PreviewMeshComponent))
|
|
{
|
|
Result = ESkeletonTreeFilterResult::Hidden;
|
|
}
|
|
|
|
// Remove non-vertex-weighted bones if we're filtering
|
|
else if (BoneFilter == EBoneFilter::LOD && !BoneItem->IsBoneRequired(BoneMeshIndex, PreviewMeshComponent))
|
|
{
|
|
Result = ESkeletonTreeFilterResult::Hidden;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else if (InItem->IsOfType<FSkeletonTreeSocketItem>())
|
|
{
|
|
TSharedPtr<FSkeletonTreeSocketItem> SocketItem = StaticCastSharedPtr<FSkeletonTreeSocketItem>(InItem);
|
|
|
|
if (SocketFilter == ESocketFilter::None)
|
|
{
|
|
Result = ESkeletonTreeFilterResult::Hidden;
|
|
}
|
|
|
|
// Remove non-mesh sockets if we're filtering
|
|
else if ((SocketFilter == ESocketFilter::Mesh || SocketFilter == ESocketFilter::None) && SocketItem->GetParentType() == ESocketParentType::Skeleton)
|
|
{
|
|
Result = ESkeletonTreeFilterResult::Hidden;
|
|
}
|
|
|
|
// Remove non-skeleton sockets if we're filtering
|
|
else if ((SocketFilter == ESocketFilter::Skeleton || SocketFilter == ESocketFilter::None) && SocketItem->GetParentType() == ESocketParentType::Mesh)
|
|
{
|
|
Result = ESkeletonTreeFilterResult::Hidden;
|
|
}
|
|
|
|
else if (SocketFilter == ESocketFilter::Active && SocketItem->GetParentType() == ESocketParentType::Skeleton && SocketItem->IsSocketCustomized())
|
|
{
|
|
// Don't add the skeleton socket if it's already added for the mesh
|
|
Result = ESkeletonTreeFilterResult::Hidden;
|
|
}
|
|
}
|
|
}
|
|
|
|
return Result;
|
|
}
|
|
|
|
void SSkeletonTree::HandleSelectedBonesChanged(const TArray<FName>& InBoneNames, ESelectInfo::Type InSelectInfo)
|
|
{
|
|
SetSelectedBones(InBoneNames, InSelectInfo);
|
|
}
|
|
|
|
void SSkeletonTree::HandleSelectedSocketChanged(const FSelectedSocketInfo& InSocketInfo)
|
|
{
|
|
SetSelectedSocket(InSocketInfo);
|
|
}
|
|
|
|
void SSkeletonTree::HandleDeselectAll()
|
|
{
|
|
DeselectAll();
|
|
}
|
|
|
|
#undef LOCTEXT_NAMESPACE
|