// 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 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>) .TreeItemsSource(&RootNodes) .SelectionMode(ESelectionMode::Multi) .OnGenerateRow(this, &SDatabaseAssetTree::MakeTableRowWidget) .OnGetChildren(this, &SDatabaseAssetTree::HandleGetChildrenForTree) .OnContextMenuOpening(this, &SDatabaseAssetTree::CreateContextMenu) .HighlightParentNodesForSelection(false) .OnSelectionChanged_Lambda([this](TSharedPtr Item, ESelectInfo::Type Type) { TArray> 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 Operation = DragDropEvent.GetOperation(); const bool bValidOperation = Operation.IsValid() && (Operation->IsOfType() || Operation->IsOfType()); if (bValidOperation) { Reply = AssetUtil::CanHandleAssetDrag(DragDropEvent); if (!Reply.IsEventHandled()) { if (Operation->IsOfType()) { const TSharedPtr AssetDragDropOp = StaticCastSharedPtr(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>& TransactionObjectContexts) const { // Ensure that we only react to modifications to the UPosesSearchDatabase. if (const TSharedPtr ViewModel = EditorViewModel.Pin()) { if (const UPoseSearchDatabase* Database = ViewModel->GetPoseSearchDatabase()) { for (const TPair& 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 ViewModel = EditorViewModel.Pin(); if (!ViewModel.IsValid()) { return; } const TSharedRef 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> PreviouslySelectedNodes = TreeView->GetSelectedItems(); // Rebuild node hierarchy { // Build an index based off of alphabetical order than iterate the index instead TArray 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(SequenceIdxA); if (!A) { return false; } const FPoseSearchDatabaseAnimationAssetBase* B = Database->GetDatabaseAnimationAsset(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(MappedId)) { const bool bFiltered = (DatabaseAnimationAsset->GetAnimationAsset() == nullptr || GetAssetFilterString().IsEmpty()) ? false : !DatabaseAnimationAsset->GetName().Contains(GetAssetFilterString()); if (!bFiltered) { // Create sequence node const TSharedPtr SequenceGroupNode = MakeShared(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& RootNode : RootNodes) { TreeView->SetItemExpansion(RootNode, true); } // Handle selection if (bRecoverSelection) { RecoverSelection(PreviouslySelectedNodes); } else { TreeView->SetItemSelection(PreviouslySelectedNodes, false, ESelectInfo::Direct); } } TSharedRef SDatabaseAssetTree::MakeTableRowWidget( TSharedPtr InItem, const TSharedRef& OwnerTable) { return InItem->MakeTreeRowWidget(OwnerTable, InItem.ToSharedRef(), CommandList.ToSharedRef(), SharedThis(this)); } void SDatabaseAssetTree::HandleGetChildrenForTree( TSharedPtr InNode, TArray>& OutChildren) { OutChildren = InNode.Get()->Children; } TOptional SDatabaseAssetTree::OnCanAcceptDrop( const FDragDropEvent& DragDropEvent, EItemDropZone DropZone, TSharedPtr TargetItem) { TOptional ReturnedDropZone; TSharedPtr Operation = DragDropEvent.GetOperation(); const bool bValidOperation = Operation.IsValid() && Operation->IsOfType(); if (bValidOperation) { const TSharedPtr AssetDragDropOp = StaticCastSharedPtr(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 TargetItem) { const TSharedPtr Operation = DragDropEvent.GetOperation(); const bool bValidOperation = Operation.IsValid() && Operation->IsOfType(); if (!bValidOperation) { return FReply::Unhandled(); } const TSharedPtr ViewModel = EditorViewModel.Pin(); if (!ViewModel.IsValid()) { return FReply::Unhandled(); } TArray 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(Asset)); ++AddedAssets; } if (AssetClass->IsChildOf(UAnimComposite::StaticClass())) { ViewModel->AddAnimCompositeToDatabase(Cast(Asset)); ++AddedAssets; } else if (AssetClass->IsChildOf(UBlendSpace::StaticClass())) { ViewModel->AddBlendSpaceToDatabase(Cast(Asset)); ++AddedAssets; } else if (AssetClass->IsChildOf(UAnimMontage::StaticClass())) { ViewModel->AddAnimMontageToDatabase(Cast(Asset)); ++AddedAssets; } else if (AssetClass->IsChildOf(UMultiAnimAsset::StaticClass())) { ViewModel->AddMultiAnimAssetToDatabase(Cast(Asset)); ++AddedAssets; } } GWarn->EndSlowTask(); } if (AddedAssets == 0) { return FReply::Unhandled(); } FinalizeTreeChanges(false); return FReply::Handled(); } TSharedRef 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 SDatabaseAssetTree::CreateContextMenu() { const bool CloseAfterSelection = true; FMenuBuilder MenuBuilder(CloseAfterSelection, CommandList); const TArray> SelectedNodes = TreeView->GetSelectedItems(); if (!SelectedNodes.IsEmpty()) { MenuBuilder.BeginSection("SelectedAssetsEdit", LOCTEXT("SelectedAssetEdit", "Asset Actions")); { const TSharedPtr 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 SDatabaseAssetTree::GenerateFilterBoxWidget() { TSharedPtr 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 Node, bool bFinalizeChanges) { FScopedTransaction Transaction(LOCTEXT("DeleteAsset", "Delete Asset")); const TSharedPtr 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>& PreviouslySelectedNodes) { TArray> NewSelectedNodes; for (const TSharedPtr& Node : AllNodes) { const bool bFoundNode = PreviouslySelectedNodes.ContainsByPredicate([Node](const TSharedPtr& 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(); 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> SelectedNodes = TreeView->GetSelectedItems(); for (const TSharedPtr& SelectedNode : SelectedNodes) { if (SelectedNode->SourceAssetIdx != INDEX_NONE) { return true; } } return false; } void SDatabaseAssetTree::OnDeleteNodes() { TArray> SelectedNodes = TreeView->GetSelectedItems(); if (!SelectedNodes.IsEmpty()) { const FScopedTransaction Transaction(LOCTEXT("DeletePoseSearchDatabaseNodes", "Delete selected item(s) from Pose Search Database")); const TSharedPtr ViewModel = EditorViewModel.Pin(); SelectedNodes.Sort([](const TSharedPtr& A, const TSharedPtr& B) { return B->SourceAssetIdx < A->SourceAssetIdx; }); for (const TSharedPtr& SelectedNode : SelectedNodes) { if (SelectedNode->SourceAssetIdx != INDEX_NONE) { OnDeleteAsset(SelectedNode, false); } } ViewModel->RemovePreviewActors(); FinalizeTreeChanges(); } } void SDatabaseAssetTree::OnCopySelectedNodesToClipboard() const { TArray> 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 ViewModel = EditorViewModel.Pin(); for (const TSharedPtr& SelectedNode : SelectedNodes) { if (SelectedNode->SourceAssetIdx != INDEX_NONE) { if (UPoseSearchDatabase* Database = ViewModel->GetPoseSearchDatabase()) { if (const FPoseSearchDatabaseAnimationAssetBase* DatabaseAnimationAsset = Database->GetDatabaseAnimationAsset(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 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> SelectedNodes = TreeView->GetSelectedItems(); if (!SelectedNodes.IsEmpty()) { const TSharedPtr ViewModel = EditorViewModel.Pin(); SelectedNodes.Sort([](const TSharedPtr& A, const TSharedPtr& B) { return B->SourceAssetIdx < A->SourceAssetIdx; }); for (const TSharedPtr& SelectedNode : SelectedNodes) { if (SelectedNode->SourceAssetIdx != INDEX_NONE) { if (UPoseSearchDatabase* Database = ViewModel->GetPoseSearchDatabase()) { const FPoseSearchDatabaseAnimationAssetBase* DatabaseAnimationAsset = Database->GetDatabaseAnimationAsset(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 ViewModel = EditorViewModel.Pin(); if (UPoseSearchDatabase* PoseSearchDatabase = ViewModel->GetPoseSearchDatabase()) { TArray> 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& SelectedNode : SelectedNodes) { ViewModel->SetIsEnabled(SelectedNode->SourceAssetIdx, bIsEnabled); } FinalizeTreeChanges(); } } } void SDatabaseAssetTree::OnSetMirrorOptionForSelectedNodes(EPoseSearchMirrorOption InMirrorOption) { const TSharedPtr ViewModel = EditorViewModel.Pin(); if (UPoseSearchDatabase* PoseSearchDatabase = ViewModel->GetPoseSearchDatabase()) { TArray> SelectedNodes = TreeView->GetSelectedItems(); if (!SelectedNodes.IsEmpty()) { const FScopedTransaction Transaction(LOCTEXT("OnClickEditMirrorOptionPoseSearchDatabase", "Edit Mirror Option on selected items")); PoseSearchDatabase->Modify(); for (const TSharedPtr& SelectedNode : SelectedNodes) { ViewModel->SetMirrorOption(SelectedNode->SourceAssetIdx, InMirrorOption); } FinalizeTreeChanges(); } } } void SDatabaseAssetTree::OnSetPoseReselectionForSelectedNodes(bool bIsEnabled) { const TSharedPtr ViewModel = EditorViewModel.Pin(); if (UPoseSearchDatabase* PoseSearchDatabase = ViewModel->GetPoseSearchDatabase()) { TArray> SelectedNodes = TreeView->GetSelectedItems(); if (!SelectedNodes.IsEmpty()) { const FScopedTransaction Transaction(LOCTEXT("OnClickEditPoseReselection", "Set pose reselection for selected items")); PoseSearchDatabase->Modify(); for (const TSharedPtr& SelectedNode : SelectedNodes) { ViewModel->SetDisableReselection(SelectedNode->SourceAssetIdx, !bIsEnabled); } RefreshTreeView(false, true); } } } void SDatabaseAssetTree::OnConvertToBranchIn() { const TSharedPtr ViewModel = EditorViewModel.Pin(); if (UPoseSearchDatabase* PoseSearchDatabase = ViewModel->GetPoseSearchDatabase()) { TArray> 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& SelectedNode : SelectedNodes) { if (FPoseSearchDatabaseAnimationAssetBase* DatabaseAnimationAssetBase = PoseSearchDatabase->GetMutableDatabaseAnimationAsset(SelectedNode->SourceAssetIdx)) { if (UAnimSequenceBase* AnimSequenceBase = Cast(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(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& Node : AllNodes) { if (Node->SourceAssetIdx == SourceAssetIdx) { TreeView->SetItemSelection(Node, true); } } } } } #undef LOCTEXT_NAMESPACE