// Copyright Epic Games, Inc. All Rights Reserved. #include "SSkeletonSlotNames.h" #include "Framework/Notifications/NotificationManager.h" #include "Widgets/Notifications/SNotificationList.h" #include "Misc/ScopedSlowTask.h" #include "Framework/MultiBox/MultiBoxDefs.h" #include "Framework/MultiBox/MultiBoxBuilder.h" #include "Animation/Skeleton.h" #include "Styling/AppStyle.h" #include "Layout/WidgetPath.h" #include "Framework/Application/MenuStack.h" #include "Framework/Application/SlateApplication.h" #include "Animation/AnimMontage.h" #include "FileHelpers.h" #include "SSlotNameReferenceWindow.h" #include "AssetRegistry/AssetRegistryModule.h" #include "ScopedTransaction.h" #include "Widgets/Input/SSearchBox.h" #include "Widgets/Text/SInlineEditableTextBlock.h" #include "Widgets/Input/STextEntryPopup.h" #include "Animation/AnimBlueprint.h" #include "AnimGraphNode_Slot.h" #include "Kismet2/BlueprintEditorUtils.h" #include "Interfaces/IMainFrameModule.h" #include "IEditableSkeleton.h" #include "TabSpawners.h" #define LOCTEXT_NAMESPACE "SkeletonSlotNames" static const FName ColumnId_SlotNameLabel( "SlotName" ); ////////////////////////////////////////////////////////////////////////// // SMorphTargetListRow typedef TSharedPtr< FDisplayedSlotNameInfo > FDisplayedSlotNameInfoPtr; class SSlotNameListRow : public SMultiColumnTableRow< FDisplayedSlotNameInfoPtr > { public: SLATE_BEGIN_ARGS( SSlotNameListRow ) {} /** The item for this row **/ SLATE_ARGUMENT( FDisplayedSlotNameInfoPtr, Item ) /* Widget used to display the list of morph targets */ SLATE_ARGUMENT( TSharedPtr, SlotNameListView ) SLATE_END_ARGS() void Construct( const FArguments& InArgs, const TSharedRef& OwnerTableView ); /** Overridden from SMultiColumnTableRow. Generates a widget for this column of the tree row. */ virtual TSharedRef GenerateWidgetForColumn( const FName& ColumnName ) override; private: /** Widget used to display the list of slot name */ TSharedPtr SlotNameListView; /** The notify being displayed by this row */ FDisplayedSlotNameInfoPtr Item; }; void SSlotNameListRow::Construct( const FArguments& InArgs, const TSharedRef& InOwnerTableView ) { Item = InArgs._Item; SlotNameListView = InArgs._SlotNameListView; check( Item.IsValid() ); SMultiColumnTableRow< FDisplayedSlotNameInfoPtr >::Construct( FSuperRowType::FArguments(), InOwnerTableView ); } TSharedRef< SWidget > SSlotNameListRow::GenerateWidgetForColumn( const FName& ColumnName ) { check( ColumnName == ColumnId_SlotNameLabel ); // Items can be either Slots or Groups. FText ItemText = Item->bIsGroupItem ? FText::Format(LOCTEXT("AnimSlotManagerGroupItem", "(Group) {0}"), FText::FromName(Item->Name)) : FText::Format(LOCTEXT("AnimSlotManagerSlotItem", "(Slot) {0}"), FText::FromName(Item->Name)); return SNew(SHorizontalBox) + SHorizontalBox::Slot() .AutoWidth() [ SNew(SExpanderArrow, SharedThis(this)) ] + SHorizontalBox::Slot() .AutoWidth() [ SNew(STextBlock) .Text(ItemText) ]; } ///////////////////////////////////////////////////// // FSkeletonSlotNamesSummoner FSkeletonSlotNamesSummoner::FSkeletonSlotNamesSummoner(TSharedPtr InHostingApp, const TSharedRef& InEditableSkeleton, FOnObjectSelected InOnObjectSelected) : FWorkflowTabFactory(FPersonaTabs::SkeletonSlotNamesID, InHostingApp) , EditableSkeleton(InEditableSkeleton) , OnObjectSelected(InOnObjectSelected) { TabLabel = LOCTEXT("AnimSlotManagerTabTitle", "Anim Slot Manager"); TabIcon = FSlateIcon(FAppStyle::GetAppStyleSetName(), "Persona.Tabs.AnimSlotManager"); EnableTabPadding(); bIsSingleton = true; ViewMenuDescription = LOCTEXT("SkeletonSlotNamesMenu", "Anim Slots"); ViewMenuTooltip = LOCTEXT("SkeletonSlotNames_ToolTip", "Manage Skeleton's Slots and Groups."); } TSharedRef FSkeletonSlotNamesSummoner::CreateTabBody(const FWorkflowTabSpawnInfo& Info) const { return SNew(SSkeletonSlotNames, EditableSkeleton.Pin().ToSharedRef()) .OnObjectSelected(OnObjectSelected); } ///////////////////////////////////////////////////// // SSkeletonSlotNames void SSkeletonSlotNames::Construct(const FArguments& InArgs, const TSharedRef& InEditableSkeleton) { EditableSkeleton = InEditableSkeleton; OnObjectSelected = InArgs._OnObjectSelected; InEditableSkeleton->RegisterOnSlotsChanged(FSimpleMulticastDelegate::FDelegate::CreateSP(this, &SSkeletonSlotNames::RefreshSlotNameListWithFilter)); // Toolbar FToolBarBuilder ToolbarBuilder(TSharedPtr< FUICommandList >(), FMultiBoxCustomization::None); // Save USkeleton ToolbarBuilder.AddToolBarButton( FUIAction(FExecuteAction::CreateSP(this, &SSkeletonSlotNames::OnSaveSkeleton)) , NAME_None , LOCTEXT("AnimSlotManagerToolbarSaveLabel", "Save") , LOCTEXT("AnimSlotManagerToolbarSaveTooltip", "Saves changes into Skeleton asset") , FSlateIcon(FAppStyle::GetAppStyleSetName(), "AnimSlotManager.SaveSkeleton") ); ToolbarBuilder.AddSeparator(); // Add Slot ToolbarBuilder.AddToolBarButton( FUIAction(FExecuteAction::CreateSP(this, &SSkeletonSlotNames::OnAddSlot)) , NAME_None , LOCTEXT("AnimSlotManagerToolbarAddSlotLabel", "Add Slot") , LOCTEXT("AnimSlotManagerToolbarAddSlotTooltip", "Create a new unique Slot name") , FSlateIcon(FAppStyle::GetAppStyleSetName(), "AnimSlotManager.AddSlot") ); // Add Group ToolbarBuilder.AddToolBarButton( FUIAction(FExecuteAction::CreateSP(this, &SSkeletonSlotNames::OnAddGroup)) , NAME_None , LOCTEXT("AnimSlotManagerToolbarAddGroupLabel", "Add Group") , LOCTEXT("AnimSlotManagerToolbarAddGroupTooltip", "Create a new unique Group name") , FSlateIcon(FAppStyle::GetAppStyleSetName(), "AnimSlotManager.AddGroup") ); this->ChildSlot [ SNew( SVerticalBox ) + SVerticalBox::Slot() .AutoHeight() [ ToolbarBuilder.MakeWidget() ] + SVerticalBox::Slot() .AutoHeight() .Padding( FMargin( 0.0f, 0.0f, 0.0f, 4.0f ) ) [ SAssignNew( NameFilterBox, SSearchBox ) .SelectAllTextWhenFocused( true ) .OnTextChanged( this, &SSkeletonSlotNames::OnFilterTextChanged ) .OnTextCommitted( this, &SSkeletonSlotNames::OnFilterTextCommitted ) .HintText( LOCTEXT( "AnimSlotManagerSlotNameSearchBoxHint", "Slot name filter...") ) ] + SVerticalBox::Slot() .FillHeight( 1.0f ) // This is required to make the scrollbar work, as content overflows Slate containers by default [ SAssignNew( SlotNameListView, SSlotNameListType ) .TreeItemsSource(&NotifyList) .OnGenerateRow( this, &SSkeletonSlotNames::GenerateNotifyRow ) .OnGetChildren(this, &SSkeletonSlotNames::GetChildrenForInfo) .OnContextMenuOpening( this, &SSkeletonSlotNames::OnGetContextMenuContent ) .SelectionMode(ESelectionMode::Single) .OnSelectionChanged( this, &SSkeletonSlotNames::OnNotifySelectionChanged ) .HeaderRow ( SNew( SHeaderRow ) + SHeaderRow::Column( ColumnId_SlotNameLabel ) .DefaultLabel( LOCTEXT( "SlotNameNameLabel", "Slot Name" ) ) ) ] ]; CreateSlotNameList(); } void SSkeletonSlotNames::OnFilterTextChanged( const FText& SearchText ) { FilterText = SearchText; RefreshSlotNameListWithFilter(); } void SSkeletonSlotNames::OnFilterTextCommitted( const FText& SearchText, ETextCommit::Type CommitInfo ) { // Just do the same as if the user typed in the box OnFilterTextChanged( SearchText ); } TSharedRef SSkeletonSlotNames::GenerateNotifyRow(TSharedPtr InInfo, const TSharedRef& OwnerTable) { check( InInfo.IsValid() ); return SNew( SSlotNameListRow, OwnerTable ) .Item( InInfo ) .SlotNameListView( SharedThis( this ) ); } void SSkeletonSlotNames::GetChildrenForInfo(TSharedPtr InInfo, TArray< TSharedPtr >& OutChildren) { check(InInfo.IsValid()); OutChildren = InInfo->Children; } TSharedPtr SSkeletonSlotNames::OnGetContextMenuContent() const { const bool bShouldCloseWindowAfterMenuSelection = true; FMenuBuilder MenuBuilder( bShouldCloseWindowAfterMenuSelection, NULL); TArray< TSharedPtr< FDisplayedSlotNameInfo > > SelectedItems = SlotNameListView.Get()->GetSelectedItems(); bool bHasSelectedItem = (SelectedItems.Num() > 0); bool bShowGroupItem = bHasSelectedItem && SelectedItems[0].Get()->bIsGroupItem; bool bShowSlotItem = bHasSelectedItem && !SelectedItems[0].Get()->bIsGroupItem; if (bShowGroupItem) { TSharedPtr SelectedItemPtr = SelectedItems[0]; MenuBuilder.BeginSection("SlotManagerSlotGroupActions", LOCTEXT("SlotManagerSlotGroupActions", "Slot Group Actions")); // Delete Slot Group { FDisplayedSlotNameInfo* SlotInfo = SelectedItemPtr.Get(); FUIAction Action = FUIAction(FExecuteAction::CreateSP(const_cast(this), &SSkeletonSlotNames::OnDeleteSlotGroup, SlotInfo->Name)); Action.CanExecuteAction.BindSP(const_cast(this), &SSkeletonSlotNames::CanDeleteSlotGroup, SlotInfo->Name); const FText Label = LOCTEXT("AnimSlotManagerContextMenuDeleteSlotGroupLabel", "Delete Slot Group"); const FText ToolTipText = LOCTEXT("AnimSlotManagerContextMenuDeleteSlotGroupTooltip", "Delete this slot group."); MenuBuilder.AddMenuEntry(Label, ToolTipText, FSlateIcon(), Action); } MenuBuilder.EndSection(); } else if (bShowSlotItem) { TSharedPtr SelectedItemPtr = SelectedItems[0]; MenuBuilder.BeginSection("SlotManagerSlotActions", LOCTEXT("SlotManagerSlotActions", "Slot Actions")); // Set Slot Group { MenuBuilder.AddSubMenu( FText::Format(LOCTEXT("ContextMenuSetSlotGroupLabel", "Set Slot {0} Group to"), FText::FromName(SelectedItems[0].Get()->Name)), FText::Format(LOCTEXT("ContextMenuSetSlotGroupToolTip", "Set Slot {0} Group"), FText::FromName(SelectedItems[0].Get()->Name)), FNewMenuDelegate::CreateRaw(const_cast(this), &SSkeletonSlotNames::FillSetSlotGroupSubMenu)); } // Rename Slot { FDisplayedSlotNameInfo* SlotInfo = SelectedItemPtr.Get(); FUIAction Action = FUIAction(FExecuteAction::CreateSP(const_cast(this), &SSkeletonSlotNames::OnRenameSlot, SlotInfo->Name)); const FText Label = LOCTEXT("AnimSlotManagerContextMenuRenameSlotLabel", "Rename Slot"); const FText ToolTipText = LOCTEXT("AnimSlotManagerContextMenuRenameSlotTooltip", "Rename this slot"); MenuBuilder.AddMenuEntry(Label, ToolTipText, FSlateIcon(), Action); } // Delete Slot { FDisplayedSlotNameInfo* SlotInfo = SelectedItemPtr.Get(); FUIAction Action = FUIAction(FExecuteAction::CreateSP(const_cast(this), &SSkeletonSlotNames::OnDeleteSlot, SlotInfo->Name)); const FText Label = LOCTEXT("AnimSlotManagerContextMenuDeleteSlotLabel", "Delete Slot"); const FText ToolTipText = LOCTEXT("AnimSlotManagerContextMenuDeleteSlotTooltip", "Delete this slot."); MenuBuilder.AddMenuEntry(Label, ToolTipText, FSlateIcon(), Action); } MenuBuilder.EndSection(); } MenuBuilder.BeginSection("SlotManagerGeneralActions", LOCTEXT("SlotManagerGeneralActions", "Slot Manager Actions")); // Add Slot { FUIAction Action = FUIAction(FExecuteAction::CreateSP(const_cast(this), &SSkeletonSlotNames::OnAddSlot)); const FText Label = LOCTEXT("AnimSlotManagerContextMenuAddSlotLabel", "Add Slot"); const FText ToolTipText = LOCTEXT("AnimSlotManagerContextMenuAddSlotTooltip", "Adds a new Slot"); MenuBuilder.AddMenuEntry(Label, ToolTipText, FSlateIcon(), Action); } // Add Group { FUIAction Action = FUIAction(FExecuteAction::CreateSP(const_cast(this), &SSkeletonSlotNames::OnAddGroup)); const FText Label = LOCTEXT("AnimSlotManagerContextMenuAddGroupLabel", "Add Group"); const FText ToolTipText = LOCTEXT("AnimSlotManagerContextMenuAddGroupTooltip", "Adds a new Group"); MenuBuilder.AddMenuEntry(Label, ToolTipText, FSlateIcon(), Action); } MenuBuilder.EndSection(); return MenuBuilder.MakeWidget(); } void SSkeletonSlotNames::FillSetSlotGroupSubMenu(FMenuBuilder& MenuBuilder) { const TArray& SlotGroups = EditableSkeleton->GetSkeleton().GetSlotGroups(); for (auto SlotGroup : SlotGroups) { const FName& GroupName = SlotGroup.GroupName; const FText ToolTipText = FText::Format(LOCTEXT("ContextMenuSetSlotSubMenuToolTip", "Changes slot's group to {0}"), FText::FromName(GroupName)); /* FString Label = Class->GetDisplayNameText().ToString();*/ const FText Label = FText::FromName(GroupName); FUIAction UIAction; UIAction.ExecuteAction.BindRaw(this, &SSkeletonSlotNames::ContextMenuOnSetSlot, GroupName); MenuBuilder.AddMenuEntry(Label, ToolTipText, FSlateIcon(), UIAction); } } void SSkeletonSlotNames::ContextMenuOnSetSlot(FName InNewGroupName) { TArray< TSharedPtr< FDisplayedSlotNameInfo > > SelectedItems = SlotNameListView.Get()->GetSelectedItems(); bool bHasSelectedItem = (SelectedItems.Num() > 0); bool bShowSlotItem = bHasSelectedItem && !SelectedItems[0].Get()->bIsGroupItem; if (bShowSlotItem) { const FName SlotName = SelectedItems[0].Get()->Name; if (EditableSkeleton->GetSkeleton().ContainsSlotName(SlotName)) { EditableSkeleton->SetSlotGroupName(SlotName, InNewGroupName); RefreshSlotNameListWithFilter(); } // Highlight newly created item. TSharedPtr< FDisplayedSlotNameInfo > Item = FindItemNamed(SlotName); if (Item.IsValid()) { SlotNameListView->SetSelection(Item); } FSlateApplication::Get().DismissAllMenus(); } } void SSkeletonSlotNames::OnNotifySelectionChanged(TSharedPtr Selection, ESelectInfo::Type SelectInfo) { if(Selection.IsValid()) { ShowNotifyInDetailsView(Selection->Name); } } void SSkeletonSlotNames::OnSaveSkeleton() { TArray< UPackage* > PackagesToSave; PackagesToSave.Add(EditableSkeleton->GetSkeleton().GetOutermost()); FEditorFileUtils::PromptForCheckoutAndSave(PackagesToSave, /*bCheckDirty=*/ false, /*bPromptToSave=*/ false); } void SSkeletonSlotNames::OnAddSlot() { TSharedRef TextEntry = SNew(STextEntryPopup) .Label(LOCTEXT("NewSlotName_AskSlotName", "New Slot Name")) .OnTextCommitted(this, &SSkeletonSlotNames::AddSlotPopUpOnCommit); // Show dialog to enter new track name FSlateApplication::Get().PushMenu( SharedThis(this), FWidgetPath(), TextEntry, FSlateApplication::Get().GetCursorPos(), FPopupTransitionEffect(FPopupTransitionEffect::TypeInPopup) ); } void SSkeletonSlotNames::OnAddGroup() { TSharedRef TextEntry = SNew(STextEntryPopup) .Label(LOCTEXT("NewGroupName_AskGroupName", "New Group Name")) .OnTextCommitted(this, &SSkeletonSlotNames::AddGroupPopUpOnCommit); // Show dialog to enter new track name FSlateApplication::Get().PushMenu( SharedThis(this), FWidgetPath(), TextEntry, FSlateApplication::Get().GetCursorPos(), FPopupTransitionEffect(FPopupTransitionEffect::TypeInPopup) ); } void SSkeletonSlotNames::AddSlotPopUpOnCommit(const FText & InNewSlotText, ETextCommit::Type CommitInfo) { if (!InNewSlotText.IsEmpty()) { const FScopedTransaction Transaction(LOCTEXT("NewSlotName_AddSlotName", "Add New Slot Node Name")); FName NewSlotName = FName(*InNewSlotText.ToString()); const USkeleton& Skeleton = EditableSkeleton->GetSkeleton(); // Keep slot and group names unique if (!Skeleton.ContainsSlotName(NewSlotName) && (Skeleton.FindAnimSlotGroup(NewSlotName) == nullptr)) { TArray< TSharedPtr< FDisplayedSlotNameInfo > > SelectedItems = SlotNameListView->GetSelectedItems(); bool bHasSelectedItem = (SelectedItems.Num() > 0); bool bShowGroupItem = bHasSelectedItem && SelectedItems[0].Get()->bIsGroupItem; EditableSkeleton->SetSlotGroupName(NewSlotName, bShowGroupItem ? SelectedItems[0].Get()->Name : FAnimSlotGroup::DefaultGroupName); RefreshSlotNameListWithFilter(); } // Highlight newly created item. TSharedPtr< FDisplayedSlotNameInfo > Item = FindItemNamed(NewSlotName); if (Item.IsValid()) { SlotNameListView->SetSelection(Item); } FSlateApplication::Get().DismissAllMenus(); } } void SSkeletonSlotNames::AddGroupPopUpOnCommit(const FText & InNewGroupText, ETextCommit::Type CommitInfo) { if (!InNewGroupText.IsEmpty()) { FName NewGroupName = FName(*InNewGroupText.ToString()); const USkeleton& Skeleton = EditableSkeleton->GetSkeleton(); // Keep slot and group names unique if (!Skeleton.ContainsSlotName(NewGroupName) && EditableSkeleton->AddSlotGroupName(NewGroupName)) { RefreshSlotNameListWithFilter(); } // Highlight newly created item. TSharedPtr< FDisplayedSlotNameInfo > Item = FindItemNamed(NewGroupName); if (Item.IsValid()) { SlotNameListView->SetSelection(Item); } FSlateApplication::Get().DismissAllMenus(); } } void SSkeletonSlotNames::GetCompatibleAnimBlueprints( TArray& OutAssets ) { //Get the skeleton tag to search for const USkeleton& Skeleton = EditableSkeleton->GetSkeleton(); FString SkeletonExportName = FAssetData(&Skeleton).GetExportTextName(); // Load the asset registry module FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked(TEXT("AssetRegistry")); TArray AssetDataList; AssetRegistryModule.Get().GetAssetsByClass(UAnimBlueprint::StaticClass()->GetClassPathName(), AssetDataList, true); OutAssets.Empty(AssetDataList.Num()); for(FAssetData& Data : AssetDataList) { const FString AssetSkeleton = Data.GetTagValueRef("TargetSkeleton"); if(AssetSkeleton == SkeletonExportName) { OutAssets.Add(Data); } } } void SSkeletonSlotNames::RefreshSlotNameListWithFilter() { CreateSlotNameList( NameFilterBox->GetText().ToString() ); } void SSkeletonSlotNames::CreateSlotNameList(const FString& SearchText) { NotifyList.Empty(); const USkeleton& Skeleton = EditableSkeleton->GetSkeleton(); const TArray& SlotGroups = Skeleton.GetSlotGroups(); for (auto SlotGroup : SlotGroups) { const FName& GroupName = SlotGroup.GroupName; TSharedRef GroupItem = FDisplayedSlotNameInfo::Make(GroupName, true); SlotNameListView->SetItemExpansion(GroupItem, true); NotifyList.Add(GroupItem); for (auto SlotName : SlotGroup.SlotNames) { if (SearchText.IsEmpty() || GroupName.ToString().Contains(SearchText) || SlotName.ToString().Contains(SearchText)) { TSharedRef SlotItem = FDisplayedSlotNameInfo::Make(SlotName, false); SlotNameListView->SetItemExpansion(SlotItem, true); NotifyList[NotifyList.Num() - 1]->Children.Add(SlotItem); } } } SlotNameListView->RequestTreeRefresh(); } TSharedPtr< FDisplayedSlotNameInfo > SSkeletonSlotNames::FindItemNamed(FName ItemName) const { for (auto SlotGroupItem : NotifyList) { if (SlotGroupItem->Name == ItemName) { return SlotGroupItem; } for (auto SlotItem : SlotGroupItem->Children) { if (SlotItem->Name == ItemName) { return SlotItem; } } } return NULL; } void SSkeletonSlotNames::ShowNotifyInDetailsView(FName NotifyName) { // @todo nothing to show now, but in the future // we can show the list of montage that are used by this slot node? } void SSkeletonSlotNames::GetCompatibleAnimMontages(TArray& OutAssets) { //Get the skeleton tag to search for const USkeleton& Skeleton = EditableSkeleton->GetSkeleton(); FString SkeletonExportName = FAssetData(&Skeleton).GetExportTextName(); // Load the asset registry module FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked(TEXT("AssetRegistry")); TArray AssetDataList; AssetRegistryModule.Get().GetAssetsByClass(UAnimMontage::StaticClass()->GetClassPathName(), AssetDataList, true); OutAssets.Empty(AssetDataList.Num()); for( int32 AssetIndex = 0; AssetIndex < AssetDataList.Num(); ++AssetIndex ) { const FAssetData& PossibleAnimMontage = AssetDataList[AssetIndex]; if( SkeletonExportName == PossibleAnimMontage.GetTagValueRef("Skeleton") ) { OutAssets.Add( PossibleAnimMontage ); } } } UObject* SSkeletonSlotNames::ShowInDetailsView( UClass* EdClass ) { UObject *Obj = EditorObjectTracker.GetEditorObjectForClass(EdClass); if(Obj != NULL) { OnObjectSelected.ExecuteIfBound(Obj); } return Obj; } void SSkeletonSlotNames::ClearDetailsView() { OnObjectSelected.ExecuteIfBound(nullptr); } void SSkeletonSlotNames::PostUndoRedo() { RefreshSlotNameListWithFilter(); } void SSkeletonSlotNames::AddReferencedObjects( FReferenceCollector& Collector ) { EditorObjectTracker.AddReferencedObjects(Collector); } void SSkeletonSlotNames::NotifyUser( FNotificationInfo& NotificationInfo ) { TSharedPtr Notification = FSlateNotificationManager::Get().AddNotification( NotificationInfo ); if ( Notification.IsValid() ) { Notification->SetCompletionState( SNotificationItem::CS_Fail ); } } void SSkeletonSlotNames::OnDeleteSlot(FName SlotName) { TArray CompatibleMontages; TMultiMap CompatibleSlotNodes; GetMontagesAndNodesUsingSlot(SlotName, CompatibleMontages, CompatibleSlotNodes); if(CompatibleMontages.Num() > 0 || CompatibleSlotNodes.Num() > 0) { // We can't delete here - still have references. Give the user a chance to fix. if(!ReferenceWindow.IsValid()) { // No existing window SAssignNew(ReferenceWindow, SWindow) .AutoCenter(EAutoCenter::PreferredWorkArea) .SizingRule(ESizingRule::Autosized) .Title(LOCTEXT("ReferenceWindowTitle", "Slot References")); ReferenceWindow->SetContent ( SNew(SBorder) .Padding(FMargin(3)) .BorderImage(FAppStyle::GetBrush("ToolPanel.GroupBorder")) [ SAssignNew(ReferenceWidget, SSlotNameReferenceWindow) .ReferencingMontages(&CompatibleMontages) .ReferencingNodes(&CompatibleSlotNodes) .SlotName(SlotName.ToString()) .OperationText(LOCTEXT("DeleteOperation", "Delete")) .WidgetWindow(ReferenceWindow) .OnRetry(FSimpleDelegate::CreateSP(this, &SSkeletonSlotNames::RetryDeleteSlot, SlotName)) ] ); TSharedPtr ParentWindow; IMainFrameModule& MainFrameModule = FModuleManager::LoadModuleChecked(TEXT("MainFrame")); ParentWindow = MainFrameModule.GetParentWindow(); FSlateApplication::Get().AddWindowAsNativeChild(ReferenceWindow.ToSharedRef(), ParentWindow.ToSharedRef()); ReferenceWindow->SetOnWindowClosed(FOnWindowClosed::CreateSP(this, &SSkeletonSlotNames::ReferenceWindowClosed)); } else { TSharedPtr RefWidgetPinned = ReferenceWidget.Pin(); if(RefWidgetPinned.IsValid()) { FReferenceWindowInfo WindowInfo; WindowInfo.ReferencingMontages = &CompatibleMontages; WindowInfo.ReferencingNodes = &CompatibleSlotNodes; WindowInfo.ItemText = FText::FromName(SlotName); WindowInfo.OperationText = LOCTEXT("DeleteOperation", "Delete"); WindowInfo.RetryDelegate = FSimpleDelegate::CreateSP(this, &SSkeletonSlotNames::RetryDeleteSlot, SlotName); RefWidgetPinned->UpdateInfo(WindowInfo); ReferenceWindow->BringToFront(); } } } else { DeleteSlot(SlotName); } } void SSkeletonSlotNames::DeleteSlot(const FName& SlotName) { const USkeleton& Skeleton = EditableSkeleton->GetSkeleton(); if(Skeleton.ContainsSlotName(SlotName)) { EditableSkeleton->DeleteSlotName(SlotName); RefreshSlotNameListWithFilter(); } } void SSkeletonSlotNames::GetAnimMontagesUsingSlot(FName SlotName, TArray& OutMontages) { TArray SkeletonCompatibleMontages; GetCompatibleAnimMontages(SkeletonCompatibleMontages); for(FAssetData& MontageData : SkeletonCompatibleMontages) { UAnimMontage* Montage = Cast(MontageData.GetAsset()); check(Montage); for(FSlotAnimationTrack& SlotTrack : Montage->SlotAnimTracks) { if(SlotTrack.SlotName == SlotName) { OutMontages.Add(MontageData); } } } } void SSkeletonSlotNames::GetAnimMontagesUsingSlotGroup(FName SlotGroupName, TArray& OutMontages) { if(const FAnimSlotGroup* Group = EditableSkeleton->GetSkeleton().FindAnimSlotGroup(SlotGroupName)) { for(const FName& SlotName : Group->SlotNames) { GetAnimMontagesUsingSlot(SlotName, OutMontages); } } } void SSkeletonSlotNames::ReferenceWindowClosed(const TSharedRef& Window) { ReferenceWindow = nullptr; } void SSkeletonSlotNames::RetryDeleteSlot(FName SlotName) { TArray CompatibleMontages; TMultiMap CompatibleSlotNodes; GetMontagesAndNodesUsingSlot(SlotName, CompatibleMontages, CompatibleSlotNodes); if(CompatibleMontages.Num() > 0 || CompatibleSlotNodes.Num() > 0) { // Still can't delete TSharedPtr PinnedWidget = ReferenceWidget.Pin(); if(PinnedWidget.IsValid()) { FReferenceWindowInfo WindowInfo; WindowInfo.ReferencingMontages = &CompatibleMontages; WindowInfo.ReferencingNodes = &CompatibleSlotNodes; WindowInfo.ItemText = FText::FromName(SlotName); WindowInfo.OperationText = LOCTEXT("DeleteOperation", "Delete"); WindowInfo.RetryDelegate = FSimpleDelegate::CreateSP(this, &SSkeletonSlotNames::RetryDeleteSlot, SlotName); PinnedWidget->UpdateInfo(WindowInfo); ReferenceWindow->BringToFront(); } } else { ReferenceWindow->RequestDestroyWindow(); DeleteSlot(SlotName); } } void SSkeletonSlotNames::GetMontagesAndNodesUsingSlot(const FName& SlotName, TArray& CompatibleMontages, TMultiMap &CompatibleSlotNodes) { FScopedSlowTask SlowTask(3, LOCTEXT("AssetReferenceSlowTaskMessage", "Checking for slot references.")); SlowTask.MakeDialog(); TArray CompatibleBlueprints; SlowTask.EnterProgressFrame(1, LOCTEXT("AssetReferenceTask_Blueprints", "Searching Blueprints")); GetCompatibleAnimBlueprints(CompatibleBlueprints); SlowTask.EnterProgressFrame(1, LOCTEXT("AssetReferenceTask_Montages", "Searching Montages")); GetAnimMontagesUsingSlot(SlotName, CompatibleMontages); SlowTask.EnterProgressFrame(1, LOCTEXT("AssetReferenceTask_Nodes", "Searching Nodes")); for(FAssetData& Data : CompatibleBlueprints) { TArray BPGraphs; UAnimBlueprint* AnimBP = Cast(Data.GetAsset()); AnimBP->GetAllGraphs(BPGraphs); for(UEdGraph* Graph : BPGraphs) { TArray SlotNodes; Graph->GetNodesOfClass(SlotNodes); for(UAnimGraphNode_Slot* SlotNode : SlotNodes) { if(SlotNode->Node.SlotName == SlotName) { CompatibleSlotNodes.Add(AnimBP, SlotNode); } } } } // If we end up loading in any previously unsaved assets they can add names to the list so refresh RefreshSlotNameListWithFilter(); } void SSkeletonSlotNames::GetMontagesAndNodesUsingSlotGroup(const FName& SlotGroupName, TArray& OutMontages, TMultiMap &OutBlueprintSlotMap) { if(const FAnimSlotGroup* SlotGroup = EditableSkeleton->GetSkeleton().FindAnimSlotGroup(SlotGroupName)) { for(const auto& SlotName : SlotGroup->SlotNames) { GetMontagesAndNodesUsingSlot(SlotName, OutMontages, OutBlueprintSlotMap); } } } void SSkeletonSlotNames::OnRenameSlotPopupCommitted(const FText & InNewSlotText, ETextCommit::Type CommitInfo, FName OldName) { if(CommitInfo == ETextCommit::OnEnter) { FName NewName(*InNewSlotText.ToString()); // Need to dismiss menus early or the slow task in GetMontagesAndNodesUsingSlot will fail to show onscreen FSlateApplication::Get().DismissAllMenus(); // Make sure the name doesn't already exist if(EditableSkeleton->GetSkeleton().ContainsSlotName(NewName)) { FNotificationInfo Notification(FText::Format(LOCTEXT("ToastRenameFailDesc", "Rename Failed! Slot name {0} already exists in the target skeleton."), FText::FromName(NewName))); Notification.ExpireDuration = 3.0f; Notification.bFireAndForget = true; NotifyUser(Notification); return; } // Validate references TArray CompatibleMontages; TMultiMap CompatibleSlotNodes; GetMontagesAndNodesUsingSlot(OldName, CompatibleMontages, CompatibleSlotNodes); if(CompatibleMontages.Num() > 0 || CompatibleSlotNodes.Num() > 0) { // We can't rename here - still have references. Give the user a chance to fix. if(!ReferenceWindow.IsValid()) { // No existing window SAssignNew(ReferenceWindow, SWindow) .AutoCenter(EAutoCenter::PreferredWorkArea) .SizingRule(ESizingRule::Autosized) .Title(LOCTEXT("ReferenceWindowTitle", "Slot References")); ReferenceWindow->SetContent ( SNew(SBorder) .Padding(FMargin(3)) .BorderImage(FAppStyle::GetBrush("ToolPanel.GroupBorder")) [ SAssignNew(ReferenceWidget, SSlotNameReferenceWindow) .ReferencingMontages(&CompatibleMontages) .ReferencingNodes(&CompatibleSlotNodes) .SlotName(OldName.ToString()) .OperationText(LOCTEXT("RenameOperation", "Rename")) .WidgetWindow(ReferenceWindow) .OnRetry(FSimpleDelegate::CreateSP(this, &SSkeletonSlotNames::RetryRenameSlot, OldName, NewName)) ] ); TSharedPtr ParentWindow; IMainFrameModule& MainFrameModule = FModuleManager::LoadModuleChecked(TEXT("MainFrame")); ParentWindow = MainFrameModule.GetParentWindow(); FSlateApplication::Get().AddWindowAsNativeChild(ReferenceWindow.ToSharedRef(), ParentWindow.ToSharedRef()); ReferenceWindow->SetOnWindowClosed(FOnWindowClosed::CreateSP(this, &SSkeletonSlotNames::ReferenceWindowClosed)); } else { TSharedPtr RefWidgetPinned = ReferenceWidget.Pin(); if(RefWidgetPinned.IsValid()) { FReferenceWindowInfo WindowInfo; WindowInfo.ReferencingMontages = &CompatibleMontages; WindowInfo.ReferencingNodes = &CompatibleSlotNodes; WindowInfo.ItemText = FText::FromName(OldName); WindowInfo.OperationText = LOCTEXT("RenameOperation", "Rename"); WindowInfo.RetryDelegate = FSimpleDelegate::CreateSP(this, &SSkeletonSlotNames::RetryRenameSlot, OldName, NewName); RefWidgetPinned->UpdateInfo(WindowInfo); ReferenceWindow->BringToFront(); } } } else { RenameSlot(OldName, NewName); } } } void SSkeletonSlotNames::OnRenameSlot(FName CurrentName) { TSharedRef TextEntry = SNew(STextEntryPopup) .Label(LOCTEXT("RenameSlotName_AskSlotName", "New Slot Name")) .OnTextCommitted(this, &SSkeletonSlotNames::OnRenameSlotPopupCommitted, CurrentName); // Show dialog to enter new track name FSlateApplication::Get().PushMenu( SharedThis(this), FWidgetPath(), TextEntry, FSlateApplication::Get().GetCursorPos(), FPopupTransitionEffect(FPopupTransitionEffect::TypeInPopup) ); } void SSkeletonSlotNames::RenameSlot(FName CurrentName, FName NewName) { if(EditableSkeleton->GetSkeleton().ContainsSlotName(CurrentName)) { EditableSkeleton->RenameSlotName(CurrentName, NewName); RefreshSlotNameListWithFilter(); } } void SSkeletonSlotNames::RetryRenameSlot(FName CurrentName, FName NewName) { TArray CompatibleMontages; TMultiMap CompatibleSlotNodes; GetMontagesAndNodesUsingSlot(CurrentName, CompatibleMontages, CompatibleSlotNodes); if(CompatibleMontages.Num() > 0 || CompatibleSlotNodes.Num() > 0) { // Still can't rename TSharedPtr PinnedWidget = ReferenceWidget.Pin(); if(PinnedWidget.IsValid()) { FReferenceWindowInfo WindowInfo; WindowInfo.ReferencingMontages = &CompatibleMontages; WindowInfo.ReferencingNodes = &CompatibleSlotNodes; WindowInfo.ItemText = FText::FromName(CurrentName); WindowInfo.OperationText = LOCTEXT("DeleteOperation", "Delete"); WindowInfo.RetryDelegate = FSimpleDelegate::CreateSP(this, &SSkeletonSlotNames::RetryRenameSlot, CurrentName, NewName); PinnedWidget->UpdateInfo(WindowInfo); ReferenceWindow->BringToFront(); } } else { ReferenceWindow->RequestDestroyWindow(); RenameSlot(CurrentName, NewName); } } void SSkeletonSlotNames::OnDeleteSlotGroup(FName GroupName) { TArray CompatibleMontages; TMultiMap CompatibleSlotNodes; GetMontagesAndNodesUsingSlotGroup(GroupName, CompatibleMontages, CompatibleSlotNodes); if(CompatibleMontages.Num() > 0 || CompatibleSlotNodes.Num() > 0) { // Can't delete, still referenced // We can't rename here - still have references. Give the user a chance to fix. if(!ReferenceWindow.IsValid()) { // No existing window SAssignNew(ReferenceWindow, SWindow) .AutoCenter(EAutoCenter::PreferredWorkArea) .SizingRule(ESizingRule::Autosized) .Title(LOCTEXT("ReferenceWindowTitle", "Slot References")); ReferenceWindow->SetContent ( SNew(SBorder) .Padding(FMargin(3)) .BorderImage(FAppStyle::GetBrush("ToolPanel.GroupBorder")) [ SAssignNew(ReferenceWidget, SSlotNameReferenceWindow) .ReferencingMontages(&CompatibleMontages) .ReferencingNodes(&CompatibleSlotNodes) .SlotName(GroupName.ToString()) .OperationText(LOCTEXT("DeleteGroupOperation", "Delete Group")) .WidgetWindow(ReferenceWindow) .OnRetry(FSimpleDelegate::CreateSP(this, &SSkeletonSlotNames::RetryDeleteSlotGroup, GroupName)) ] ); TSharedPtr ParentWindow; IMainFrameModule& MainFrameModule = FModuleManager::LoadModuleChecked(TEXT("MainFrame")); ParentWindow = MainFrameModule.GetParentWindow(); FSlateApplication::Get().AddWindowAsNativeChild(ReferenceWindow.ToSharedRef(), ParentWindow.ToSharedRef()); ReferenceWindow->SetOnWindowClosed(FOnWindowClosed::CreateSP(this, &SSkeletonSlotNames::ReferenceWindowClosed)); } else { TSharedPtr RefWidgetPinned = ReferenceWidget.Pin(); if(RefWidgetPinned.IsValid()) { FReferenceWindowInfo WindowInfo; WindowInfo.ReferencingMontages = &CompatibleMontages; WindowInfo.ReferencingNodes = &CompatibleSlotNodes; WindowInfo.ItemText = FText::FromName(GroupName); WindowInfo.OperationText = LOCTEXT("DeleteGroupOperation", "Delete Group"); WindowInfo.RetryDelegate = FSimpleDelegate::CreateSP(this, &SSkeletonSlotNames::RetryDeleteSlotGroup, GroupName); RefWidgetPinned->UpdateInfo(WindowInfo); ReferenceWindow->BringToFront(); } } } else { DeleteSlotGroup(GroupName); } } void SSkeletonSlotNames::RetryDeleteSlotGroup(FName GroupName) { TArray CompatibleMontages; TMultiMap CompatibleSlotNodes; GetMontagesAndNodesUsingSlotGroup(GroupName, CompatibleMontages, CompatibleSlotNodes); if(CompatibleMontages.Num() > 0 || CompatibleSlotNodes.Num() > 0) { // Still can't rename TSharedPtr PinnedWidget = ReferenceWidget.Pin(); if(PinnedWidget.IsValid()) { FReferenceWindowInfo WindowInfo; WindowInfo.ReferencingMontages = &CompatibleMontages; WindowInfo.ReferencingNodes = &CompatibleSlotNodes; WindowInfo.ItemText = FText::FromName(GroupName); WindowInfo.OperationText = LOCTEXT("DeleteGroupOperation", "Delete Group"); WindowInfo.RetryDelegate = FSimpleDelegate::CreateSP(this, &SSkeletonSlotNames::RetryDeleteSlotGroup, GroupName); PinnedWidget->UpdateInfo(WindowInfo); ReferenceWindow->BringToFront(); } } else { ReferenceWindow->RequestDestroyWindow(); DeleteSlotGroup(GroupName); } } bool SSkeletonSlotNames::CanDeleteSlotGroup(FName GroupName) { static const FName DefaultGroupName("DefaultGroup"); return GroupName != DefaultGroupName; } void SSkeletonSlotNames::DeleteSlotGroup(const FName& GroupName) { if(EditableSkeleton->GetSkeleton().FindAnimSlotGroup(GroupName) != nullptr) { EditableSkeleton->DeleteSlotGroup(GroupName); RefreshSlotNameListWithFilter(); } } #undef LOCTEXT_NAMESPACE