Files
UnrealEngine/Engine/Plugins/Animation/PoseSearch/Source/Editor/Private/PoseSearchDatabaseAssetTree.cpp
2025-05-18 13:04:45 +08:00

1148 lines
38 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "PoseSearchDatabaseAssetTree.h"
#include "Animation/AnimComposite.h"
#include "Animation/AnimMontage.h"
#include "Animation/AnimSequence.h"
#include "Animation/BlendSpace.h"
#include "AnimationBlueprintLibrary.h"
#include "AssetSelection.h"
#include "ClassIconFinder.h"
#include "DetailColumnSizeData.h"
#include "DragAndDrop/AssetDragDropOp.h"
#include "Framework/Commands/GenericCommands.h"
#include "Framework/MultiBox/MultiBoxBuilder.h"
#include "Misc/FeedbackContext.h"
#include "Misc/TransactionObjectEvent.h"
#include "PoseSearch/MultiAnimAsset.h"
#include "PoseSearch/PoseSearchAnimNotifies.h"
#include "PoseSearch/PoseSearchDatabase.h"
#include "PoseSearchDatabaseEditorClipboard.h"
#include "PoseSearchDatabaseViewModel.h"
#include "PoseSearchEditor.h"
#include "SPositiveActionButton.h"
#include "Styling/AppStyle.h"
#include "ScopedTransaction.h"
#include "Styling/StyleColors.h"
#include "Widgets/Text/SRichTextBlock.h"
#include "Widgets/Input/SCheckBox.h"
#include "Widgets/Input/SSearchBox.h"
#define LOCTEXT_NAMESPACE "SDatabaseAssetTree"
namespace UE::PoseSearch
{
SDatabaseAssetTree::~SDatabaseAssetTree()
{
}
void SDatabaseAssetTree::Construct(
const FArguments& InArgs,
TSharedRef<FDatabaseViewModel> InEditorViewModel)
{
EditorViewModel = InEditorViewModel;
CreateCommandList();
ChildSlot
[
SNew(SVerticalBox)
+SVerticalBox::Slot()
.AutoHeight()
[
SNew(SHorizontalBox)
+SHorizontalBox::Slot()
.AutoWidth()
.VAlign(VAlign_Center)
.Padding(0, 0, 4, 0)
[
SNew(SPositiveActionButton)
.Icon(FAppStyle::Get().GetBrush("Icons.Plus"))
.Text(LOCTEXT("AddNew", "Add"))
.ToolTipText(LOCTEXT("AddNewToolTip", "Add a new Sequence, Blend Space, Anim Composite, or Anim Montage"))
.OnGetMenuContent(this, &SDatabaseAssetTree::CreateAddNewMenuWidget)
]
+ SHorizontalBox::Slot()
.AutoWidth()
.VAlign(VAlign_Center)
.HAlign(HAlign_Right)
.Padding(2, 0, 0, 0)
[
GenerateFilterBoxWidget()
]
]
+SVerticalBox::Slot()
.Padding(0.0f, 0.0f)
[
SNew(SBorder)
.Padding(2.0f)
.BorderImage(FAppStyle::GetBrush("SCSEditor.TreePanel"))
[
SNew(SOverlay)
+SOverlay::Slot()
[
SAssignNew(TreeView, STreeView<TSharedPtr<FDatabaseAssetTreeNode>>)
.TreeItemsSource(&RootNodes)
.SelectionMode(ESelectionMode::Multi)
.OnGenerateRow(this, &SDatabaseAssetTree::MakeTableRowWidget)
.OnGetChildren(this, &SDatabaseAssetTree::HandleGetChildrenForTree)
.OnContextMenuOpening(this, &SDatabaseAssetTree::CreateContextMenu)
.HighlightParentNodesForSelection(false)
.OnSelectionChanged_Lambda([this](TSharedPtr<FDatabaseAssetTreeNode> Item, ESelectInfo::Type Type)
{
TArray<TSharedPtr<FDatabaseAssetTreeNode>> SelectedItems = TreeView->GetSelectedItems();
OnSelectionChanged.Broadcast(SelectedItems, Type);
})
]
+SOverlay::Slot()
[
SAssignNew(TreeViewDragAndDropSuggestion, SVerticalBox)
+ SVerticalBox::Slot()
.HAlign(HAlign_Center)
.VAlign(VAlign_Center)
[
SNew(STextBlock)
.Text(FText::FromString(TEXT("Drag and drop Animation Sequences, Anim Composites, Blendspaces, or Anim Montages")))
.Font(FAppStyle::Get().GetFontStyle("DetailsView.CategoryFontStyle"))
]
]
]
]
];
RefreshTreeView(true);
}
FReply SDatabaseAssetTree::OnDragOver(const FGeometry& MyGeometry, const FDragDropEvent& DragDropEvent)
{
FReply Reply = FReply::Unhandled();
TSharedPtr<FDragDropOperation> Operation = DragDropEvent.GetOperation();
const bool bValidOperation =
Operation.IsValid() &&
(Operation->IsOfType<FExternalDragOperation>() || Operation->IsOfType<FAssetDragDropOp>());
if (bValidOperation)
{
Reply = AssetUtil::CanHandleAssetDrag(DragDropEvent);
if (!Reply.IsEventHandled())
{
if (Operation->IsOfType<FAssetDragDropOp>())
{
const TSharedPtr<FAssetDragDropOp> AssetDragDropOp = StaticCastSharedPtr<FAssetDragDropOp>(Operation);
for (const FAssetData& AssetData : AssetDragDropOp->GetAssets())
{
if (UClass* AssetClass = AssetData.GetClass())
{
if (AssetClass->IsChildOf(UAnimSequence::StaticClass()) ||
AssetClass->IsChildOf(UAnimComposite::StaticClass()) ||
AssetClass->IsChildOf(UBlendSpace::StaticClass()) ||
AssetClass->IsChildOf(UAnimMontage::StaticClass()) ||
AssetClass->IsChildOf(UMultiAnimAsset::StaticClass()))
{
Reply = FReply::Handled();
break;
}
}
}
}
}
}
return Reply;
}
FReply SDatabaseAssetTree::OnDrop(const FGeometry& MyGeometry, const FDragDropEvent& DragDropEvent)
{
return OnAcceptDrop(DragDropEvent, EItemDropZone::OntoItem, nullptr);
}
FReply SDatabaseAssetTree::OnKeyDown(const FGeometry& MyGeometry, const FKeyEvent& InKeyEvent)
{
if (CommandList->ProcessCommandBindings(InKeyEvent))
{
return FReply::Handled();
}
return FReply::Unhandled();
}
bool SDatabaseAssetTree::MatchesContext(const FTransactionContext& InContext, const TArray<TPair<UObject*, FTransactionObjectEvent>>& TransactionObjectContexts) const
{
// Ensure that we only react to modifications to the UPosesSearchDatabase.
if (const TSharedPtr<FDatabaseViewModel> ViewModel = EditorViewModel.Pin())
{
if (const UPoseSearchDatabase* Database = ViewModel->GetPoseSearchDatabase())
{
for (const TPair<UObject*, FTransactionObjectEvent>& TransactionObjectPair : TransactionObjectContexts)
{
const UObject* Object = TransactionObjectPair.Key;
while (Object != nullptr)
{
if (Object == Database)
{
return true;
}
Object = Object->GetOuter();
}
}
}
}
return false;
}
void SDatabaseAssetTree::PostUndo(bool bSuccess)
{
if (bSuccess)
{
FinalizeTreeChanges();
}
}
void SDatabaseAssetTree::PostRedo(bool bSuccess)
{
if (bSuccess)
{
FinalizeTreeChanges();
}
}
void SDatabaseAssetTree::RefreshTreeView(bool bIsInitialSetup, bool bRecoverSelection)
{
const TSharedPtr<FDatabaseViewModel> ViewModel = EditorViewModel.Pin();
if (!ViewModel.IsValid())
{
return;
}
const TSharedRef<FDatabaseViewModel> ViewModelRef = ViewModel.ToSharedRef();
// Empty node data.
RootNodes.Reset();
AllNodes.Reset();
const UPoseSearchDatabase* Database = ViewModel->GetPoseSearchDatabase();
if (!IsValid(Database))
{
TreeView->RequestTreeRefresh();
return;
}
// Store selection so we can recover it afterwards (if possible)
TArray<TSharedPtr<FDatabaseAssetTreeNode>> PreviouslySelectedNodes = TreeView->GetSelectedItems();
// Rebuild node hierarchy
{
// Build an index based off of alphabetical order than iterate the index instead
TArray<uint32> IndexArray;
IndexArray.SetNumUninitialized(Database->GetNumAnimationAssets());
for (int32 AnimationAssetIdx = 0; AnimationAssetIdx < Database->GetNumAnimationAssets(); ++AnimationAssetIdx)
{
IndexArray[AnimationAssetIdx] = AnimationAssetIdx;
}
IndexArray.Sort([Database](int32 SequenceIdxA, int32 SequenceIdxB)
{
const FPoseSearchDatabaseAnimationAssetBase* A = Database->GetDatabaseAnimationAsset<FPoseSearchDatabaseAnimationAssetBase>(SequenceIdxA);
if (!A)
{
return false;
}
const FPoseSearchDatabaseAnimationAssetBase* B = Database->GetDatabaseAnimationAsset<FPoseSearchDatabaseAnimationAssetBase>(SequenceIdxB);
if (!B)
{
return true;
}
//If its null add it to the end of the list
if (!B->GetAnimationAsset())
{
return true;
}
if (!A->GetAnimationAsset())
{
return false;
}
const int32 Comparison = A->GetName().Compare(B->GetName());
return Comparison < 0;
});
// create all nodes
for (int32 AnimationAssetIdx = 0; AnimationAssetIdx < Database->GetNumAnimationAssets(); ++AnimationAssetIdx)
{
const int32 MappedId = IndexArray[AnimationAssetIdx];
if (const FPoseSearchDatabaseAnimationAssetBase* DatabaseAnimationAsset = Database->GetDatabaseAnimationAsset<FPoseSearchDatabaseAnimationAssetBase>(MappedId))
{
const bool bFiltered = (DatabaseAnimationAsset->GetAnimationAsset() == nullptr || GetAssetFilterString().IsEmpty()) ? false : !DatabaseAnimationAsset->GetName().Contains(GetAssetFilterString());
if (!bFiltered)
{
// Create sequence node
const TSharedPtr<FDatabaseAssetTreeNode> SequenceGroupNode = MakeShared<FDatabaseAssetTreeNode>(MappedId, ViewModelRef);
// Keep track of node
RootNodes.Add(SequenceGroupNode);
AllNodes.Add(SequenceGroupNode);
}
}
}
// Show drag and drop suggestion if tree is empty
TreeViewDragAndDropSuggestion->SetVisibility(IndexArray.IsEmpty() ? EVisibility::Visible : EVisibility::Hidden);
}
// Update tree view
TreeView->RequestTreeRefresh();
for (TSharedPtr<FDatabaseAssetTreeNode>& RootNode : RootNodes)
{
TreeView->SetItemExpansion(RootNode, true);
}
// Handle selection
if (bRecoverSelection)
{
RecoverSelection(PreviouslySelectedNodes);
}
else
{
TreeView->SetItemSelection(PreviouslySelectedNodes, false, ESelectInfo::Direct);
}
}
TSharedRef<ITableRow> SDatabaseAssetTree::MakeTableRowWidget(
TSharedPtr<FDatabaseAssetTreeNode> InItem,
const TSharedRef<STableViewBase>& OwnerTable)
{
return InItem->MakeTreeRowWidget(OwnerTable, InItem.ToSharedRef(), CommandList.ToSharedRef(), SharedThis(this));
}
void SDatabaseAssetTree::HandleGetChildrenForTree(
TSharedPtr<FDatabaseAssetTreeNode> InNode,
TArray<TSharedPtr<FDatabaseAssetTreeNode>>& OutChildren)
{
OutChildren = InNode.Get()->Children;
}
TOptional<EItemDropZone> SDatabaseAssetTree::OnCanAcceptDrop(
const FDragDropEvent& DragDropEvent,
EItemDropZone DropZone,
TSharedPtr<FDatabaseAssetTreeNode> TargetItem)
{
TOptional<EItemDropZone> ReturnedDropZone;
TSharedPtr<FDragDropOperation> Operation = DragDropEvent.GetOperation();
const bool bValidOperation = Operation.IsValid() && Operation->IsOfType<FAssetDragDropOp>();
if (bValidOperation)
{
const TSharedPtr<FAssetDragDropOp> AssetDragDropOp = StaticCastSharedPtr<FAssetDragDropOp>(Operation);
for (const FAssetData& AssetData : AssetDragDropOp->GetAssets())
{
if (UClass* AssetClass = AssetData.GetClass())
{
if (AssetClass->IsChildOf(UAnimSequence::StaticClass()) ||
AssetClass->IsChildOf(UAnimComposite::StaticClass()) ||
AssetClass->IsChildOf(UBlendSpace::StaticClass()) ||
AssetClass->IsChildOf(UAnimMontage::StaticClass()) ||
AssetClass->IsChildOf(UMultiAnimAsset::StaticClass()))
{
ReturnedDropZone = EItemDropZone::OntoItem;
break;
}
}
}
}
return ReturnedDropZone;
}
FReply SDatabaseAssetTree::OnAcceptDrop(
const FDragDropEvent& DragDropEvent,
EItemDropZone DropZone,
TSharedPtr<FDatabaseAssetTreeNode> TargetItem)
{
const TSharedPtr<FDragDropOperation> Operation = DragDropEvent.GetOperation();
const bool bValidOperation = Operation.IsValid() && Operation->IsOfType<FAssetDragDropOp>();
if (!bValidOperation)
{
return FReply::Unhandled();
}
const TSharedPtr<FDatabaseViewModel> ViewModel = EditorViewModel.Pin();
if (!ViewModel.IsValid())
{
return FReply::Unhandled();
}
TArray<FAssetData> DroppedAssetData = AssetUtil::ExtractAssetDataFromDrag(Operation);
const int32 NumAssets = DroppedAssetData.Num();
int32 AddedAssets = 0;
UPoseSearchDatabase* PoseSearchDatabase = ViewModel->GetPoseSearchDatabase();
if (PoseSearchDatabase && NumAssets > 0)
{
GWarn->BeginSlowTask(LOCTEXT("LoadingAssets", "Loading Asset(s)"), true);
const FScopedTransaction Transaction(LOCTEXT("AddAssetsOnDrop", "Add Animation Asset(s) to Pose Search Database"));
PoseSearchDatabase->Modify();
for (int32 DroppedAssetIdx = 0; DroppedAssetIdx < NumAssets; ++DroppedAssetIdx)
{
const FAssetData& AssetData = DroppedAssetData[DroppedAssetIdx];
if (!AssetData.IsAssetLoaded())
{
GWarn->StatusUpdate(
DroppedAssetIdx,
NumAssets,
FText::Format(
LOCTEXT("LoadingAsset", "Loading Asset {0}"),
FText::FromName(AssetData.AssetName)));
}
UClass* AssetClass = AssetData.GetClass();
UObject* Asset = AssetData.GetAsset();
if (AssetClass->IsChildOf(UAnimSequence::StaticClass()))
{
ViewModel->AddSequenceToDatabase(Cast<UAnimSequence>(Asset));
++AddedAssets;
}
if (AssetClass->IsChildOf(UAnimComposite::StaticClass()))
{
ViewModel->AddAnimCompositeToDatabase(Cast<UAnimComposite>(Asset));
++AddedAssets;
}
else if (AssetClass->IsChildOf(UBlendSpace::StaticClass()))
{
ViewModel->AddBlendSpaceToDatabase(Cast<UBlendSpace>(Asset));
++AddedAssets;
}
else if (AssetClass->IsChildOf(UAnimMontage::StaticClass()))
{
ViewModel->AddAnimMontageToDatabase(Cast<UAnimMontage>(Asset));
++AddedAssets;
}
else if (AssetClass->IsChildOf(UMultiAnimAsset::StaticClass()))
{
ViewModel->AddMultiAnimAssetToDatabase(Cast<UMultiAnimAsset>(Asset));
++AddedAssets;
}
}
GWarn->EndSlowTask();
}
if (AddedAssets == 0)
{
return FReply::Unhandled();
}
FinalizeTreeChanges(false);
return FReply::Handled();
}
TSharedRef<SWidget> SDatabaseAssetTree::CreateAddNewMenuWidget()
{
FMenuBuilder AddOptions(true, nullptr);
AddOptions.BeginSection("AddOptions", LOCTEXT("AssetAddOptions", "Assets"));
{
AddOptions.AddMenuEntry(
LOCTEXT("AddSequenceOption", "Sequence"),
LOCTEXT("AddSequenceToDatabaseTooltip", "Add new sequence to the database"),
FSlateIcon(),
FUIAction(FExecuteAction::CreateSP(this, &SDatabaseAssetTree::OnAddSequence, true)),
NAME_None,
EUserInterfaceActionType::Button);
AddOptions.AddMenuEntry(
LOCTEXT("AddBlendSpaceOption", "Blend Space"),
LOCTEXT("AddBlendSpaceToDatabaseTooltip", "Add new blend space to the database"),
FSlateIcon(),
FUIAction(FExecuteAction::CreateSP(this, &SDatabaseAssetTree::OnAddBlendSpace, true)),
NAME_None,
EUserInterfaceActionType::Button);
AddOptions.AddMenuEntry(
LOCTEXT("AddAnimCompositeOption", "Anim Composite"),
LOCTEXT("AddAnimCompositeToDatabaseTooltip", "Add new composite to the database"),
FSlateIcon(),
FUIAction(FExecuteAction::CreateSP(this, &SDatabaseAssetTree::OnAddAnimComposite, true)),
NAME_None,
EUserInterfaceActionType::Button);
AddOptions.AddMenuEntry(
LOCTEXT("AddAnimMontageOption", "Anim Montage"),
LOCTEXT("AddAnimMontageToDatabaseTooltip", "Add new montage to the database"),
FSlateIcon(),
FUIAction(FExecuteAction::CreateSP(this, &SDatabaseAssetTree::OnAddAnimMontage, true)),
NAME_None,
EUserInterfaceActionType::Button);
AddOptions.AddMenuEntry(
LOCTEXT("AddMultiAnimAssetOption", "Multi Anim Asset"),
LOCTEXT("AddMultiAnimAssetToDatabaseTooltip", "Add new multi anim asset to the database"),
FSlateIcon(),
FUIAction(FExecuteAction::CreateSP(this, &SDatabaseAssetTree::OnAddMultiAnimAsset, true)),
NAME_None,
EUserInterfaceActionType::Button);
}
AddOptions.EndSection();
return AddOptions.MakeWidget();
}
TSharedPtr<SWidget> SDatabaseAssetTree::CreateContextMenu()
{
const bool CloseAfterSelection = true;
FMenuBuilder MenuBuilder(CloseAfterSelection, CommandList);
const TArray<TSharedPtr<FDatabaseAssetTreeNode>> SelectedNodes = TreeView->GetSelectedItems();
if (!SelectedNodes.IsEmpty())
{
MenuBuilder.BeginSection("SelectedAssetsEdit", LOCTEXT("SelectedAssetEdit", "Asset Actions"));
{
const TSharedPtr<FDatabaseViewModel> ViewModel = EditorViewModel.Pin();
const EPoseSearchMirrorOption MirrorOption = ViewModel->GetMirrorOption(SelectedNodes[0]->SourceAssetIdx);
FName IconToUseForSubMenu = NAME_None;
if (SelectedNodes.Num() == 1)
{
switch (MirrorOption)
{
case EPoseSearchMirrorOption::UnmirroredOnly: IconToUseForSubMenu = "GraphEditor.AlignNodesRight"; break;
case EPoseSearchMirrorOption::MirroredOnly: IconToUseForSubMenu = "GraphEditor.AlignNodesLeft"; break;
case EPoseSearchMirrorOption::UnmirroredAndMirrored: IconToUseForSubMenu = "GraphEditor.AlignNodesCenter"; break;
}
}
const FText LabelToUseForSubMenu = SelectedNodes.Num() == 1 ? LOCTEXT("SetMirrorOption", "Set Mirror Option") : LOCTEXT("SetMirrorOptionInSelectedAssets", "Set Mirror Option on selected assets");
const FText TooltipToUseForSubMenu = LOCTEXT("SetMirrorOptionTooltip", "Set the mirror option in the selected asset(s)");
MenuBuilder.AddSubMenu(
LabelToUseForSubMenu,
TooltipToUseForSubMenu,
FNewMenuDelegate::CreateLambda([this](FMenuBuilder& MenuBuilder)
{
MenuBuilder.AddMenuEntry(
LOCTEXT("OriginalOnly", "Original Only"),
LOCTEXT("OriginalOnlyTooltip", "Mirror Option: Original Only"),
FSlateIcon(FAppStyle::GetAppStyleSetName(), "GraphEditor.AlignNodesRight"),
FUIAction(FExecuteAction::CreateSP(this, &SDatabaseAssetTree::OnSetMirrorOptionForSelectedNodes, EPoseSearchMirrorOption::UnmirroredOnly)),
NAME_None,
EUserInterfaceActionType::Button
);
MenuBuilder.AddMenuEntry(
LOCTEXT("MirrorOnly", "Mirrored Only"),
LOCTEXT("MirrorOnlyTooltip", "Mirror Option: Mirrored Only"),
FSlateIcon(FAppStyle::GetAppStyleSetName(), "GraphEditor.AlignNodesLeft"),
FUIAction(FExecuteAction::CreateSP(this, &SDatabaseAssetTree::OnSetMirrorOptionForSelectedNodes, EPoseSearchMirrorOption::MirroredOnly)),
NAME_None,
EUserInterfaceActionType::Button
);
MenuBuilder.AddMenuEntry(
LOCTEXT("OriginalAndMirrorOnly", "Original and Mirrored"),
LOCTEXT("OriginalAndMirrorOnlyTooltip", "Mirror Option: Original and Mirrored"),
FSlateIcon(FAppStyle::GetAppStyleSetName(), "GraphEditor.AlignNodesCenter"),
FUIAction(FExecuteAction::CreateSP(this, &SDatabaseAssetTree::OnSetMirrorOptionForSelectedNodes, EPoseSearchMirrorOption::UnmirroredAndMirrored)),
NAME_None,
EUserInterfaceActionType::Button
);
}),
false,
FSlateIcon(FAppStyle::GetAppStyleSetName(), IconToUseForSubMenu));
if (SelectedNodes.Num() > 1)
{
MenuBuilder.AddMenuEntry(
LOCTEXT("EnableReselection", "Enable pose reselection in selected assets"),
LOCTEXT("EnableTooltipReselection", "Enable reselection of poses in the same asset"),
FSlateIcon(FAppStyle::GetAppStyleSetName(), "MotionMatchingEditor.EnablePoseReselection"),
FUIAction(FExecuteAction::CreateSP(this, &SDatabaseAssetTree::OnSetPoseReselectionForSelectedNodes, true)),
NAME_None,
EUserInterfaceActionType::Button);
MenuBuilder.AddMenuEntry(
LOCTEXT("DisableReselection", "Disable pose reselection in selected assets"),
LOCTEXT("DisableToolTipReselection", "Disable reselection of poses in the same asset"),
FSlateIcon(FAppStyle::GetAppStyleSetName(), "MotionMatchingEditor.DisablePoseReselection"),
FUIAction(FExecuteAction::CreateSP(this, &SDatabaseAssetTree::OnSetPoseReselectionForSelectedNodes, false)),
NAME_None,
EUserInterfaceActionType::Button);
}
else
{
const bool IsReselectionDisabled = ViewModel->IsDisableReselection(SelectedNodes[0]->SourceAssetIdx);
const FName IconToUse = IsReselectionDisabled ? "MotionMatchingEditor.EnablePoseReselection" : "MotionMatchingEditor.DisablePoseReselection";
const FText LabelToUse = IsReselectionDisabled ? LOCTEXT("EnablePoseReselection", "Enable pose reselection") : LOCTEXT("DisablePoseReselection", "Disable pose reselection");
const FText TooltipToUse = IsReselectionDisabled ? LOCTEXT("EnablePoseReselectionTooltip", "Enable pose reselection in the same asset") : LOCTEXT("DisablePoseReselectionTooltip", "Disable pose reselection in the same asset");
MenuBuilder.AddMenuEntry(
LabelToUse,
TooltipToUse,
FSlateIcon(FAppStyle::GetAppStyleSetName(), IconToUse),
FUIAction(FExecuteAction::CreateLambda([this, SourceAssetIndex = SelectedNodes[0]->SourceAssetIdx, ViewModel]()
{
bool IsPoseReselectionDisabled = ViewModel->IsDisableReselection(SourceAssetIndex);
ViewModel->SetDisableReselection(SourceAssetIndex, !IsPoseReselectionDisabled);
RefreshTreeView(false, true);
})),
NAME_None,
EUserInterfaceActionType::Button);
}
if (SelectedNodes.Num() > 1)
{
MenuBuilder.AddMenuEntry(
LOCTEXT("EnableSelectedAssets", "Enable selected assets"),
LOCTEXT("EnableSelectedAssetsToolTip", "Sets Assets Enabled."),
FSlateIcon(FAppStyle::GetAppStyleSetName(), "Icons.Visible"),
FUIAction(FExecuteAction::CreateSP(this, &SDatabaseAssetTree::OnEnableNodes)),
NAME_None,
EUserInterfaceActionType::Button);
MenuBuilder.AddMenuEntry(
LOCTEXT("DisableSelectedAssets", "Disable selected assets"),
LOCTEXT("DisableSelectedAssetsToolTip", "Sets Assets Disabled."),
FSlateIcon(FAppStyle::GetAppStyleSetName(), "Icons.Hidden"),
FUIAction(FExecuteAction::CreateSP(this, &SDatabaseAssetTree::OnDisableNodes)),
NAME_None,
EUserInterfaceActionType::Button);
}
else
{
const bool IsEnabled = ViewModel->IsEnabled(SelectedNodes[0]->SourceAssetIdx);
const FName IconToUse = !IsEnabled ? "Icons.Visible" : "Icons.Hidden";
const FText LabelToUse = !IsEnabled ? LOCTEXT("EnableAsset", "Enable asset") : LOCTEXT("DisableAsset", "Disable asset");
const FText TooltipToUse = !IsEnabled ? LOCTEXT("EnableAssetTooltip", "Inlcude asset in query when pose matching / motion matching.") : LOCTEXT("DisableAssetTooltip", "Exclude asset in query when pose matching / motion matching.");
MenuBuilder.AddMenuEntry(
LabelToUse,
TooltipToUse,
FSlateIcon(FAppStyle::GetAppStyleSetName(), IconToUse),
FUIAction(FExecuteAction::CreateLambda([this, SourceAssetIndex = SelectedNodes[0]->SourceAssetIdx, ViewModel]()
{
const bool IsEnabled = ViewModel->IsEnabled(SourceAssetIndex);
ViewModel->SetIsEnabled(SourceAssetIndex, !IsEnabled);
FinalizeTreeChanges();
})),
NAME_None,
EUserInterfaceActionType::Button);
}
MenuBuilder.AddMenuEntry(
LOCTEXT("ConvertToBranchIn", "Convert selected assets to sample via BranchIn notify"),
LOCTEXT("ConvertToBranchInToolTip", "Creates PoseSearchBranchIn notify state for the asset sampling range"),
FSlateIcon(),
FUIAction(FExecuteAction::CreateSP(this, &SDatabaseAssetTree::OnConvertToBranchIn)),
NAME_None,
EUserInterfaceActionType::Button);
}
MenuBuilder.EndSection();
MenuBuilder.BeginSection("SelectionAssetsClipboardEdit", LOCTEXT("SelectionAssetsClipboardEdit", "Edit"));
{
MenuBuilder.AddMenuEntry(FGenericCommands::Get().Cut);
MenuBuilder.AddMenuEntry(FGenericCommands::Get().Copy);
MenuBuilder.AddMenuEntry(FGenericCommands::Get().Paste);
MenuBuilder.AddMenuEntry(FGenericCommands::Get().Delete);
}
MenuBuilder.EndSection();
}
else
{
// Asset actions
MenuBuilder.BeginSection("Edit", LOCTEXT("EditSection", "Edit"));
MenuBuilder.AddWrapperSubMenu(
LOCTEXT("AddNewAnimAssetNoNodes", "Add"),
LOCTEXT("AddNewAnimAssetNoNodesToolTip", "Add a new Sequence, Blend Space, Anim Composite, or Anim Montage"),
FOnGetContent::CreateSP(this, &SDatabaseAssetTree::CreateAddNewMenuWidget),
FSlateIcon(FAppStyle::GetAppStyleSetName(), "Icons.Plus"));
MenuBuilder.EndSection();
// Edit / Clipboard actions
MenuBuilder.BeginSection("Clipboard", LOCTEXT("ClipboardSection", "Clipboard"));
{
MenuBuilder.AddMenuEntry(FGenericCommands::Get().Paste);
}
MenuBuilder.EndSection();
}
return MenuBuilder.MakeWidget();
}
TSharedRef<SWidget> SDatabaseAssetTree::GenerateFilterBoxWidget()
{
TSharedPtr<SSearchBox> SearchBox;
SAssignNew(SearchBox, SSearchBox)
.MinDesiredWidth(300.0f)
.InitialText(this, &SDatabaseAssetTree::GetFilterText)
.ToolTipText(FText::FromString(TEXT("Enter Asset Filter...")))
.OnTextChanged(this, &SDatabaseAssetTree::OnAssetFilterTextCommitted, ETextCommit::Default)
.OnTextCommitted(this, &SDatabaseAssetTree::OnAssetFilterTextCommitted);
return SearchBox.ToSharedRef();
}
FText SDatabaseAssetTree::GetFilterText() const
{
return FText::FromString(GetAssetFilterString());
}
void SDatabaseAssetTree::OnAssetFilterTextCommitted(const FText& InText, ETextCommit::Type CommitInfo)
{
SetAssetFilterString(InText.ToString());
RefreshTreeView(false);
}
void SDatabaseAssetTree::OnAddSequence(bool bFinalizeChanges)
{
FScopedTransaction Transaction(LOCTEXT("AddSequence", "Add Sequence"));
EditorViewModel.Pin()->AddSequenceToDatabase(nullptr);
if (bFinalizeChanges)
{
FinalizeTreeChanges();
}
}
void SDatabaseAssetTree::OnAddBlendSpace(bool bFinalizeChanges)
{
FScopedTransaction Transaction(LOCTEXT("AddBlendSpaceTransaction", "Add Blend Space"));
EditorViewModel.Pin()->AddBlendSpaceToDatabase(nullptr);
if (bFinalizeChanges)
{
FinalizeTreeChanges();
}
}
void SDatabaseAssetTree::OnAddAnimComposite(bool bFinalizeChanges)
{
FScopedTransaction Transaction(LOCTEXT("AddAnimCompositeTransaction", "Add Anim Composite"));
EditorViewModel.Pin()->AddAnimCompositeToDatabase(nullptr);
if (bFinalizeChanges)
{
FinalizeTreeChanges();
}
}
void SDatabaseAssetTree::OnAddAnimMontage(bool bFinalizeChanges)
{
FScopedTransaction Transaction(LOCTEXT("AddAnimMontageTransaction", "Add Anim Montage"));
EditorViewModel.Pin()->AddAnimMontageToDatabase(nullptr);
if (bFinalizeChanges)
{
FinalizeTreeChanges();
}
}
void SDatabaseAssetTree::OnAddMultiAnimAsset(bool bFinalizeChanges)
{
FScopedTransaction Transaction(LOCTEXT("AddMultiAnimAssetTransaction", "Add Multi Anim Asset"));
EditorViewModel.Pin()->AddMultiAnimAssetToDatabase(nullptr);
if (bFinalizeChanges)
{
FinalizeTreeChanges();
}
}
void SDatabaseAssetTree::OnDeleteAsset(TSharedPtr<FDatabaseAssetTreeNode> Node, bool bFinalizeChanges)
{
FScopedTransaction Transaction(LOCTEXT("DeleteAsset", "Delete Asset"));
const TSharedPtr<FDatabaseViewModel> ViewModel = EditorViewModel.Pin();
if (ViewModel->DeleteFromDatabase(Node->SourceAssetIdx) && bFinalizeChanges)
{
FinalizeTreeChanges();
}
}
void SDatabaseAssetTree::RegisterOnSelectionChanged(const FOnSelectionChanged& Delegate)
{
OnSelectionChanged.Add(Delegate);
}
void SDatabaseAssetTree::UnregisterOnSelectionChanged(FDelegateUserObject Unregister)
{
OnSelectionChanged.RemoveAll(Unregister);
}
void SDatabaseAssetTree::RecoverSelection(const TArray<TSharedPtr<FDatabaseAssetTreeNode>>& PreviouslySelectedNodes)
{
TArray<TSharedPtr<FDatabaseAssetTreeNode>> NewSelectedNodes;
for (const TSharedPtr<FDatabaseAssetTreeNode>& Node : AllNodes)
{
const bool bFoundNode = PreviouslySelectedNodes.ContainsByPredicate([Node](const TSharedPtr<FDatabaseAssetTreeNode>& PrevSelectedNode) { return PrevSelectedNode->SourceAssetIdx == Node->SourceAssetIdx; });
if (bFoundNode)
{
NewSelectedNodes.Add(Node);
}
}
// @todo: investigate if we should call a TreeView->ClearSelection() before TreeView->SetItemSelection
TreeView->SetItemSelection(NewSelectedNodes, true, ESelectInfo::Direct);
}
void SDatabaseAssetTree::CreateCommandList()
{
CommandList = MakeShared<FUICommandList>();
CommandList->MapAction(
FGenericCommands::Get().Delete,
FUIAction(
FExecuteAction::CreateSP(this, &SDatabaseAssetTree::OnDeleteNodes),
FCanExecuteAction::CreateSP(this, &SDatabaseAssetTree::CanDeleteNodes)));
CommandList->MapAction(
FGenericCommands::Get().Copy,
FExecuteAction::CreateSP(this, &SDatabaseAssetTree::OnCopySelectedNodesToClipboard),
FCanExecuteAction::CreateSP(this, &SDatabaseAssetTree::CanCopyToClipboard));
CommandList->MapAction(
FGenericCommands::Get().Paste,
FExecuteAction::CreateSP(this, &SDatabaseAssetTree::OnPasteNodesFromClipboard),
FCanExecuteAction::CreateSP(this, &SDatabaseAssetTree::CanPasteFromClipboard));
CommandList->MapAction(
FGenericCommands::Get().Cut,
FExecuteAction::CreateSP(this, &SDatabaseAssetTree::OnCutSelectedNodesToClipboard),
FCanExecuteAction::CreateSP(this, &SDatabaseAssetTree::CanCutToClipboard));
}
bool SDatabaseAssetTree::CanDeleteNodes() const
{
TArray<TSharedPtr<FDatabaseAssetTreeNode>> SelectedNodes = TreeView->GetSelectedItems();
for (const TSharedPtr<FDatabaseAssetTreeNode>& SelectedNode : SelectedNodes)
{
if (SelectedNode->SourceAssetIdx != INDEX_NONE)
{
return true;
}
}
return false;
}
void SDatabaseAssetTree::OnDeleteNodes()
{
TArray<TSharedPtr<FDatabaseAssetTreeNode>> SelectedNodes = TreeView->GetSelectedItems();
if (!SelectedNodes.IsEmpty())
{
const FScopedTransaction Transaction(LOCTEXT("DeletePoseSearchDatabaseNodes", "Delete selected item(s) from Pose Search Database"));
const TSharedPtr<FDatabaseViewModel> ViewModel = EditorViewModel.Pin();
SelectedNodes.Sort([](const TSharedPtr<FDatabaseAssetTreeNode>& A, const TSharedPtr<FDatabaseAssetTreeNode>& B)
{
return B->SourceAssetIdx < A->SourceAssetIdx;
});
for (const TSharedPtr<FDatabaseAssetTreeNode>& SelectedNode : SelectedNodes)
{
if (SelectedNode->SourceAssetIdx != INDEX_NONE)
{
OnDeleteAsset(SelectedNode, false);
}
}
ViewModel->RemovePreviewActors();
FinalizeTreeChanges();
}
}
void SDatabaseAssetTree::OnCopySelectedNodesToClipboard() const
{
TArray<TSharedPtr<FDatabaseAssetTreeNode>> SelectedNodes = TreeView->GetSelectedItems();
if (!SelectedNodes.IsEmpty())
{
if (UPoseSearchDatabaseEditorClipboardContent* ClipboardContent = UPoseSearchDatabaseEditorClipboardContent::Create())
{
const FScopedTransaction Transaction(LOCTEXT("CopyPoseSearchDatabaseNodes", "Copy selected item(s) from Pose Search Database"));
const TSharedPtr<FDatabaseViewModel> ViewModel = EditorViewModel.Pin();
for (const TSharedPtr<FDatabaseAssetTreeNode>& SelectedNode : SelectedNodes)
{
if (SelectedNode->SourceAssetIdx != INDEX_NONE)
{
if (UPoseSearchDatabase* Database = ViewModel->GetPoseSearchDatabase())
{
if (const FPoseSearchDatabaseAnimationAssetBase* DatabaseAnimationAsset = Database->GetDatabaseAnimationAsset<FPoseSearchDatabaseAnimationAssetBase>(SelectedNode->SourceAssetIdx))
{
// @todo: Support copying assets added via BranchIn notifies.
if (!DatabaseAnimationAsset->IsSynchronizedWithExternalDependency())
{
ClipboardContent->CopyDatabaseItem(DatabaseAnimationAsset);
}
else
{
UE_LOG(LogPoseSearchEditor, Log, TEXT("Failed to copy %s. Asset(s) with BranchIn notifies do not have clipboard support."), *DatabaseAnimationAsset->GetName())
}
}
}
}
}
ClipboardContent->CopyToClipboard();
}
else
{
UE_LOG(LogPoseSearchEditor, Warning, TEXT("Failed create clipboard object while attempting to copy data"));
}
}
}
bool SDatabaseAssetTree::CanCopyToClipboard() const
{
return !TreeView->GetSelectedItems().IsEmpty();
}
void SDatabaseAssetTree::OnPasteNodesFromClipboard()
{
const TSharedPtr<FDatabaseViewModel> ViewModel = EditorViewModel.Pin();
if (const UPoseSearchDatabaseEditorClipboardContent* ClipboardContent = UPoseSearchDatabaseEditorClipboardContent::CreateFromClipboard())
{
if (UPoseSearchDatabase* Database = ViewModel->GetPoseSearchDatabase())
{
const FScopedTransaction Transaction(LOCTEXT("PastePoseSearchDatabaseNodes", "Paste item(s) to Pose Search Database"));
ClipboardContent->PasteToDatabase(Database);
FinalizeTreeChanges();
}
}
else
{
UE_LOG(LogPoseSearchEditor, Warning, TEXT("Failed to get valid clipboard data while attempting to paste data"));
}
}
bool SDatabaseAssetTree::CanPasteFromClipboard()
{
const UPoseSearchDatabaseEditorClipboardContent* ClipboardContent = UPoseSearchDatabaseEditorClipboardContent::CreateFromClipboard();
return ClipboardContent && !ClipboardContent->DatabaseItems.IsEmpty();
}
void SDatabaseAssetTree::OnCutSelectedNodesToClipboard()
{
const FScopedTransaction Transaction(LOCTEXT("CutPoseSearchDatabaseNodes", "Cut selected item(s) from Pose Search Database"));
OnCopySelectedNodesToClipboard();
// @todo: Following code can be replaced with OnDeleteNodes() call once assets with external dependencies support copying/pasting.
TArray<TSharedPtr<FDatabaseAssetTreeNode>> SelectedNodes = TreeView->GetSelectedItems();
if (!SelectedNodes.IsEmpty())
{
const TSharedPtr<FDatabaseViewModel> ViewModel = EditorViewModel.Pin();
SelectedNodes.Sort([](const TSharedPtr<FDatabaseAssetTreeNode>& A, const TSharedPtr<FDatabaseAssetTreeNode>& B)
{
return B->SourceAssetIdx < A->SourceAssetIdx;
});
for (const TSharedPtr<FDatabaseAssetTreeNode>& SelectedNode : SelectedNodes)
{
if (SelectedNode->SourceAssetIdx != INDEX_NONE)
{
if (UPoseSearchDatabase* Database = ViewModel->GetPoseSearchDatabase())
{
const FPoseSearchDatabaseAnimationAssetBase* DatabaseAnimationAsset = Database->GetDatabaseAnimationAsset<FPoseSearchDatabaseAnimationAssetBase>(SelectedNode->SourceAssetIdx);
if (DatabaseAnimationAsset && !DatabaseAnimationAsset->IsSynchronizedWithExternalDependency())
{
OnDeleteAsset(SelectedNode, false);
}
}
}
}
ViewModel->RemovePreviewActors();
FinalizeTreeChanges();
}
}
bool SDatabaseAssetTree::CanCutToClipboard() const
{
return CanCopyToClipboard() && CanDeleteNodes();
}
void SDatabaseAssetTree::EnableSelectedNodes(bool bIsEnabled)
{
const TSharedPtr<FDatabaseViewModel> ViewModel = EditorViewModel.Pin();
if (UPoseSearchDatabase* PoseSearchDatabase = ViewModel->GetPoseSearchDatabase())
{
TArray<TSharedPtr<FDatabaseAssetTreeNode>> SelectedNodes = TreeView->GetSelectedItems();
if (!SelectedNodes.IsEmpty())
{
const FText TransactionName = bIsEnabled ? LOCTEXT("EnablePoseSearchDatabaseNodes", "Enable selected items from Pose Search Database") : LOCTEXT("DisablePoseSearchDatabaseNodes", "Disable selected items from Pose Search Database");
const FScopedTransaction Transaction(TransactionName);
PoseSearchDatabase->Modify();
for (const TSharedPtr<FDatabaseAssetTreeNode>& SelectedNode : SelectedNodes)
{
ViewModel->SetIsEnabled(SelectedNode->SourceAssetIdx, bIsEnabled);
}
FinalizeTreeChanges();
}
}
}
void SDatabaseAssetTree::OnSetMirrorOptionForSelectedNodes(EPoseSearchMirrorOption InMirrorOption)
{
const TSharedPtr<FDatabaseViewModel> ViewModel = EditorViewModel.Pin();
if (UPoseSearchDatabase* PoseSearchDatabase = ViewModel->GetPoseSearchDatabase())
{
TArray<TSharedPtr<FDatabaseAssetTreeNode>> SelectedNodes = TreeView->GetSelectedItems();
if (!SelectedNodes.IsEmpty())
{
const FScopedTransaction Transaction(LOCTEXT("OnClickEditMirrorOptionPoseSearchDatabase", "Edit Mirror Option on selected items"));
PoseSearchDatabase->Modify();
for (const TSharedPtr<FDatabaseAssetTreeNode>& SelectedNode : SelectedNodes)
{
ViewModel->SetMirrorOption(SelectedNode->SourceAssetIdx, InMirrorOption);
}
FinalizeTreeChanges();
}
}
}
void SDatabaseAssetTree::OnSetPoseReselectionForSelectedNodes(bool bIsEnabled)
{
const TSharedPtr<FDatabaseViewModel> ViewModel = EditorViewModel.Pin();
if (UPoseSearchDatabase* PoseSearchDatabase = ViewModel->GetPoseSearchDatabase())
{
TArray<TSharedPtr<FDatabaseAssetTreeNode>> SelectedNodes = TreeView->GetSelectedItems();
if (!SelectedNodes.IsEmpty())
{
const FScopedTransaction Transaction(LOCTEXT("OnClickEditPoseReselection", "Set pose reselection for selected items"));
PoseSearchDatabase->Modify();
for (const TSharedPtr<FDatabaseAssetTreeNode>& SelectedNode : SelectedNodes)
{
ViewModel->SetDisableReselection(SelectedNode->SourceAssetIdx, !bIsEnabled);
}
RefreshTreeView(false, true);
}
}
}
void SDatabaseAssetTree::OnConvertToBranchIn()
{
const TSharedPtr<FDatabaseViewModel> ViewModel = EditorViewModel.Pin();
if (UPoseSearchDatabase* PoseSearchDatabase = ViewModel->GetPoseSearchDatabase())
{
TArray<TSharedPtr<FDatabaseAssetTreeNode>> SelectedNodes = TreeView->GetSelectedItems();
if (!SelectedNodes.IsEmpty())
{
const FScopedTransaction Transaction(LOCTEXT("ConvertToBranchInTransaction", "Create PoseSearchBranchIn notify state for assets in Pose Search Database"));
bool bModified = false;
for (const TSharedPtr<FDatabaseAssetTreeNode>& SelectedNode : SelectedNodes)
{
if (FPoseSearchDatabaseAnimationAssetBase* DatabaseAnimationAssetBase = PoseSearchDatabase->GetMutableDatabaseAnimationAsset<FPoseSearchDatabaseAnimationAssetBase>(SelectedNode->SourceAssetIdx))
{
if (UAnimSequenceBase* AnimSequenceBase = Cast<UAnimSequenceBase>(DatabaseAnimationAssetBase->GetAnimationAsset()))
{
if (!bModified)
{
AnimSequenceBase->Modify();
bModified = true;
}
const FFloatInterval SamplingRange = DatabaseAnimationAssetBase->GetEffectiveSamplingRange(FVector::ZeroVector);
const float StartTime = SamplingRange.Min;
const float Duration = SamplingRange.Max - SamplingRange.Min;
const FName TrackName = "PoseSearch";
if (!UAnimationBlueprintLibrary::IsValidAnimNotifyTrackName(AnimSequenceBase, TrackName))
{
UAnimationBlueprintLibrary::AddAnimationNotifyTrack(AnimSequenceBase, TrackName, FColor::Turquoise);
}
UAnimNotifyState_PoseSearchBranchIn* PoseSearchBranchIn = CastChecked<UAnimNotifyState_PoseSearchBranchIn>(UAnimationBlueprintLibrary::AddAnimationNotifyStateEvent(AnimSequenceBase, TrackName, StartTime, Duration, UAnimNotifyState_PoseSearchBranchIn::StaticClass()));
PoseSearchBranchIn->Database = PoseSearchDatabase;
DatabaseAnimationAssetBase->BranchInId = PoseSearchBranchIn->GetBranchInId();
}
}
}
PoseSearchDatabase->SynchronizeWithExternalDependencies();
FinalizeTreeChanges();
}
}
}
void SDatabaseAssetTree::FinalizeTreeChanges(bool bRecoverSelection, bool bRefreshView)
{
if (bRefreshView)
{
RefreshTreeView(false, bRecoverSelection);
}
EditorViewModel.Pin()->BuildSearchIndex();
}
void SDatabaseAssetTree::SetSelectedItem(int32 SourceAssetIdx, bool bClearSelection)
{
if (bClearSelection)
{
TreeView->ClearSelection();
}
if (SourceAssetIdx >= 0)
{
for (TSharedPtr<FDatabaseAssetTreeNode>& Node : AllNodes)
{
if (Node->SourceAssetIdx == SourceAssetIdx)
{
TreeView->SetItemSelection(Node, true);
}
}
}
}
}
#undef LOCTEXT_NAMESPACE