// Copyright Epic Games, Inc. All Rights Reserved. #include "Animation/SkeletalMeshUtilities.h" #include "Engine/SkeletalMesh.h" #include "Misc/MessageDialog.h" #include "SlateOptMacros.h" #include "Framework/Application/SlateApplication.h" #include "Widgets/Layout/SSeparator.h" #include "Widgets/Layout/SScrollBox.h" #include "Widgets/Input/SCheckBox.h" #include "SSkeletonWidget.h" #include "AssetNotifications.h" #include "Editor.h" #define LOCTEXT_NAMESPACE "AssetTypeActions" namespace UE::SkeletalMeshUtilities { /** * FBoneCheckboxInfo * * Context data for the SDlgMergeSkeleton panel check boxes */ struct FBoneCheckboxInfo { FName BoneName; int32 BoneID; bool bUsed; }; /** * FDlgMergeSkeleton * * Wrapper class for SDlgMergeSkeleton. This class creates and launches a dialog then awaits the * result to return to the user. */ class FDlgMergeSkeleton { public: enum EResult { Cancel = 0, // No/Cancel, normal usage would stop the current action Confirm = 1, // Yes/Ok/Etc, normal usage would continue with action }; FDlgMergeSkeleton( USkeletalMesh* InMesh, USkeleton* InSkeleton ); /** Shows the dialog box and waits for the user to respond. */ EResult ShowModal(); // List of required bones for skeleton TArray RequiredBones; private: /** Cached pointer to the modal window */ TSharedPtr DialogWindow; /** Cached pointer to the merge skeleton widget */ TSharedPtr DialogWidget; /** The SkeletalMesh to merge bones from*/ USkeletalMesh* Mesh; /** The Skeleton to merge bones to */ USkeleton* Skeleton; }; /** * Slate panel for choosing which bones to merge into the skeleton */ class SDlgMergeSkeleton : public SCompoundWidget { public: SLATE_BEGIN_ARGS(SDlgMergeSkeleton) {} /** Window in which this widget resides */ SLATE_ATTRIBUTE(TSharedPtr, ParentWindow) SLATE_END_ARGS() /** * Constructs this widget * * @param InArgs The declaration data for this widget */ BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION void Construct( const FArguments& InArgs ) { UserResponse = FDlgMergeSkeleton::Cancel; ParentWindow = InArgs._ParentWindow.Get(); this->ChildSlot[ SNew(SVerticalBox) +SVerticalBox::Slot() .AutoHeight() .Padding(8.0f, 4.0f, 8.0f, 4.0f) [ SNew( STextBlock ) .Text( LOCTEXT("MergeSkeletonDlgDescription", "Would you like to add following bones to the skeleton?")) ] +SVerticalBox::Slot() .AutoHeight() .Padding(8.0f, 4.0f, 8.0f, 4.0f) [ SNew(SSeparator) ] +SVerticalBox::Slot() .Padding(8.0f, 4.0f, 8.0f, 4.0f) [ SNew(SBorder) [ SNew(SScrollBox) +SScrollBox::Slot() [ //Save this widget so we can populate it later with check boxes SAssignNew(CheckBoxContainer, SVerticalBox) ] ] ] +SVerticalBox::Slot() .AutoHeight() .HAlign(HAlign_Left) .Padding(8.0f, 4.0f, 8.0f, 4.0f) [ SNew(SUniformGridPanel) .SlotPadding(FAppStyle::GetMargin("StandardDialog.SlotPadding")) .MinDesiredSlotWidth(FAppStyle::GetFloat("StandardDialog.MinDesiredSlotWidth")) .MinDesiredSlotHeight(FAppStyle::GetFloat("StandardDialog.MinDesiredSlotHeight")) +SUniformGridPanel::Slot(0,0) [ SNew(SButton) .HAlign(HAlign_Center) .ContentPadding(FAppStyle::GetMargin("StandardDialog.ContentPadding")) .OnClicked(this, &SDlgMergeSkeleton::ChangeAllOptions, true) .Text(LOCTEXT("SkeletonMergeSelectAll", "Select All")) ] +SUniformGridPanel::Slot(1,0) [ SNew(SButton) .HAlign(HAlign_Center) .ContentPadding(FAppStyle::GetMargin("StandardDialog.ContentPadding")) .OnClicked(this, &SDlgMergeSkeleton::ChangeAllOptions, false) .Text(LOCTEXT("SkeletonMergeDeselectAll", "Deselect All")) ] ] +SVerticalBox::Slot() .AutoHeight() .Padding(8.0f, 4.0f, 8.0f, 4.0f) [ SNew(SSeparator) ] +SVerticalBox::Slot() .AutoHeight() .HAlign(HAlign_Right) .Padding(8.0f, 4.0f, 8.0f, 4.0f) [ SNew(SUniformGridPanel) .SlotPadding(FAppStyle::GetMargin("StandardDialog.SlotPadding")) .MinDesiredSlotWidth(FAppStyle::GetFloat("StandardDialog.MinDesiredSlotWidth")) .MinDesiredSlotHeight(FAppStyle::GetFloat("StandardDialog.MinDesiredSlotHeight")) +SUniformGridPanel::Slot(0,0) [ SNew(SButton) .HAlign(HAlign_Center) .ContentPadding(FAppStyle::GetMargin("StandardDialog.ContentPadding")) .OnClicked( this, &SDlgMergeSkeleton::OnButtonClick, FDlgMergeSkeleton::Confirm ) .Text(LOCTEXT("SkeletonMergeOk", "OK")) ] +SUniformGridPanel::Slot(1,0) [ SNew(SButton) .HAlign(HAlign_Center) .ContentPadding(FAppStyle::GetMargin("StandardDialog.ContentPadding")) .OnClicked( this, &SDlgMergeSkeleton::OnButtonClick, FDlgMergeSkeleton::Cancel ) .Text(LOCTEXT("SkeletonMergeCancel", "Cancel")) ] ] ]; } END_SLATE_FUNCTION_BUILD_OPTIMIZATION /** * Creates a Slate check box * * @param Label Text label for the check box * @param ButtonId The ID for the check box * @return The created check box widget */ TSharedRef CreateCheckBox( const FString& Label, int32 ButtonId ) { return SNew(SCheckBox) .IsChecked( this, &SDlgMergeSkeleton::IsCheckboxChecked, ButtonId ) .OnCheckStateChanged( this, &SDlgMergeSkeleton::OnCheckboxChanged, ButtonId ) [ SNew(STextBlock).Text(FText::FromString(Label)) ]; } /** * Returns the state of the check box * * @param ButtonId The ID for the check box * @return The status of the check box */ ECheckBoxState IsCheckboxChecked( int32 ButtonId ) const { return CheckBoxInfoMap.FindChecked(ButtonId).bUsed ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; } /** * Handler for all check box clicks * * @param NewCheckboxState The new state of the check box * @param CheckboxThatChanged The ID of the radio button that has changed. */ void OnCheckboxChanged( ECheckBoxState NewCheckboxState, int32 CheckboxThatChanged ) { FBoneCheckboxInfo& Info = CheckBoxInfoMap.FindChecked(CheckboxThatChanged); Info.bUsed = !Info.bUsed; } /** * Handler for the Select All and Deselect All buttons * * @param bNewCheckedState The new state of the check boxes */ FReply ChangeAllOptions(bool bNewCheckedState) { for(auto Iter = CheckBoxInfoMap.CreateIterator(); Iter; ++Iter) { FBoneCheckboxInfo& Info = Iter.Value(); Info.bUsed = bNewCheckedState; } return FReply::Handled(); } /** * Populated the dialog with multiple check boxes, each corresponding to a bone * * @param BoneInfos The list of Bones to populate the dialog with */ void PopulateOptions(TArray& BoneInfos) { for(auto Iter = BoneInfos.CreateIterator(); Iter; ++Iter) { FBoneCheckboxInfo& Info = (*Iter); Info.bUsed = true; CheckBoxInfoMap.Add(Info.BoneID, Info); CheckBoxContainer->AddSlot() .AutoHeight() [ CreateCheckBox(Info.BoneName.GetPlainNameString(), Info.BoneID) ]; } } /** * Returns the EResult of the button which the user pressed. Closing of the dialog * in any other way than clicking "Ok" results in this returning a "Cancel" value */ FDlgMergeSkeleton::EResult GetUserResponse() const { return UserResponse; } /** * Returns whether the user selected that bone to be used (checked its respective check box) */ bool IsBoneIncluded(int32 BoneID) { auto* Item = CheckBoxInfoMap.Find(BoneID); return Item ? Item->bUsed : false; } private: /** * Handles when a button is pressed, should be bound with appropriate EResult Key * * @param ButtonID - The return type of the button which has been pressed. */ FReply OnButtonClick(FDlgMergeSkeleton::EResult ButtonID) { ParentWindow->RequestDestroyWindow(); UserResponse = ButtonID; return FReply::Handled(); } /** Stores the users response to this dialog */ FDlgMergeSkeleton::EResult UserResponse; /** The slate container that the bone check boxes get added to */ TSharedPtr CheckBoxContainer; /** Store the check box state for each bone */ TMap CheckBoxInfoMap; /** Pointer to the window which holds this Widget, required for modal control */ TSharedPtr ParentWindow; }; FDlgMergeSkeleton::FDlgMergeSkeleton( USkeletalMesh* InMesh, USkeleton* InSkeleton ) { Mesh = InMesh; Skeleton = InSkeleton; if (FSlateApplication::IsInitialized()) { DialogWindow = SNew(SWindow) .Title( LOCTEXT("MergeSkeletonDlgTitle", "Merge Bones") ) .SupportsMinimize(false) .SupportsMaximize(false) .ClientSize(FVector2D(350, 500)); TSharedPtr DialogWrapper = SNew(SBorder) .BorderImage(FAppStyle::GetBrush("ToolPanel.GroupBorder")) .Padding(4.0f) [ SAssignNew(DialogWidget, SDlgMergeSkeleton) .ParentWindow(DialogWindow) ]; DialogWindow->SetContent(DialogWrapper.ToSharedRef()); } } FDlgMergeSkeleton::EResult FDlgMergeSkeleton::ShowModal() { RequiredBones.Empty(); TMap BoneIndicesMap; TArray BoneInfos; // Make a list of all skeleton bone list const FReferenceSkeleton& RefSkeleton = Skeleton->GetReferenceSkeleton(); for ( int32 BoneTreeId=0; BoneTreeIdGetRefSkeleton().GetRawBoneNum() ; ++RefBoneId ) { const FName& BoneName = Mesh->GetRefSkeleton().GetBoneName(RefBoneId); // if I can't find this from Skeleton if (BoneIndicesMap.Find(BoneName)==NULL) { FBoneCheckboxInfo Info; Info.BoneID = RefBoneId; Info.BoneName = BoneName; BoneInfos.Add(Info); } } if (BoneInfos.Num() == 0) { // it's all identical, but still need to return RequiredBones // for the case, where they'd like to replace the one exactly same hierarchy but different skeleton for ( int32 RefBoneId= 0 ; RefBoneId< Mesh->GetRefSkeleton().GetRawBoneNum() ; ++RefBoneId ) { const FName& BoneName = Mesh->GetRefSkeleton().GetBoneName(RefBoneId); RequiredBones.Add(RefBoneId); } return EResult::Confirm; } DialogWidget->PopulateOptions(BoneInfos); //Show Dialog GEditor->EditorAddModalWindow(DialogWindow.ToSharedRef()); EResult UserResponse = (EResult)DialogWidget->GetUserResponse(); if(UserResponse == EResult::Confirm) { for ( int32 RefBoneId= 0 ; RefBoneId< Mesh->GetRefSkeleton().GetRawBoneNum() ; ++RefBoneId ) { if ( DialogWidget->IsBoneIncluded(RefBoneId) ) { TArray ParentList; // I need to make sure parent exists first int32 ParentIndex = Mesh->GetRefSkeleton().GetParentIndex(RefBoneId); // make sure RequiredBones already have ParentIndex while (ParentIndex >= 0 ) { // if I don't have it yet if ( RequiredBones.Contains(ParentIndex)==false ) { ParentList.Add(ParentIndex); } ParentIndex = Mesh->GetRefSkeleton().GetParentIndex(ParentIndex); } if ( ParentList.Num() > 0 ) { // if we need to add parent list // add from back to front (since it's added from child to up for (int32 I=ParentList.Num()-1; I>=0; --I) { RequiredBones.Add(ParentList[I]); } } RequiredBones.Add(RefBoneId); } } } return UserResponse; } void AssignSkeletonToMesh(USkeletalMesh* SkelMesh) { if (!SkelMesh || SkelMesh->GetOutermost()->bIsCookedForEditor) { FAssetNotifications::CannotEditCookedAsset(SkelMesh); return; } // Create a skeleton asset from the selected skeletal mesh. Defaults to being in the same package/group as the skeletal mesh. TSharedRef WidgetWindow = SNew(SWindow) .Title(LOCTEXT("ChooseSkeletonWindowTitle", "Choose Skeleton")) .ClientSize(FVector2D(400,600)); TSharedPtr SkeletonSelectorWindow; WidgetWindow->SetContent ( SNew(SBorder) .BorderImage(FAppStyle::GetBrush("ToolPanel.GroupBorder")) [ SAssignNew(SkeletonSelectorWindow, SSkeletonSelectorWindow) .Object(SkelMesh) .WidgetWindow(WidgetWindow) ] ); GEditor->EditorAddModalWindow(WidgetWindow); USkeleton* SelectedSkeleton = SkeletonSelectorWindow->GetSelectedSkeleton(); // only do this if not same if ( SelectedSkeleton ) { FDlgMergeSkeleton AssetDlg( SkelMesh, SelectedSkeleton ); if(AssetDlg.ShowModal() == FDlgMergeSkeleton::Confirm) { TArray RequiredBones; RequiredBones.Append(AssetDlg.RequiredBones); if ( RequiredBones.Num() > 0 ) { // Do automatic asset generation. bool bSuccess = SelectedSkeleton->MergeBonesToBoneTree( SkelMesh, RequiredBones ); if ( bSuccess ) { if (SkelMesh->GetSkeleton() != SelectedSkeleton) { SkelMesh->SetSkeleton(SelectedSkeleton); SkelMesh->MarkPackageDirty(); } FAssetNotifications::SkeletonNeedsToBeSaved(SelectedSkeleton); } else { // if failed, ask if user would like to regenerate skeleton hierarchy if ( EAppReturnType::Yes == FMessageDialog::Open( EAppMsgType::YesNo, LOCTEXT("SkeletonMergeBones_Override", "FAILED TO MERGE BONES: \n\nThis could happen if significant hierarchical changes have been made,\ne.g. inserting a bone between nodes.\nWould you like to regenerate the skeleton from this mesh? \n\n***WARNING: THIS WILL INVALIDATE ALL ANIMATION DATA THAT IS LINKED TO THIS SKELETON***\n"))) { if ( SelectedSkeleton->RecreateBoneTree( SkelMesh ) ) { FAssetNotifications::SkeletonNeedsToBeSaved( SelectedSkeleton ); } } else { FMessageDialog::Open( EAppMsgType::Ok, LOCTEXT("SkeletonMergeBonesFailure", "Failed to merge bones to Skeleton") ); } } } else { FMessageDialog::Open( EAppMsgType::Ok, LOCTEXT("SkeletonMergeBonesFailure", "Failed to merge bones to Skeleton") ); } } } } } #undef LOCTEXT_NAMESPACE